ATM

  • Last updated on 18th May 2023

Introduction

In this long article, we’ll build a simulated terminal ATM (Automated Teller Machine) in Java. But before we dive into the code, I will explain our folder structure and the architecture.

Overview

The ATM System follows a layered architecture comprising several distinct layers. The User Interface Layer manages user interactions through the Main class, capturing input and displaying information via the command-line interface. The Controller Layer includes the ATM class, which orchestrates system operations like cash reserve management and user transactions. The Model Layer contains data models and entities like the AtmCard, CashReserve, and Transaction, representing what their name implies for the ATM card.

The Strategy Layer defines cash dispensing strategies through the CashDispenserStrategy interface and implementations like SimpleCashDispenseStrategy, which determines the cash dispensing logic. The Enums Package includes enumerations like Denomination for currency types and PaymentNetwork for payment networks. Lastly, the Models Package provides concrete implementations of ATM cards, such as VisaCard and MasterCard, extending the AtmCard class with bank and network-specific details. Our folder structure is below:

  • Controllers
  • Enums
  • Models
  • Models > ATMCards
  • Strategies

The ATM System will also support several features; like User Login, Cash Withdrawal with the system verifying the PIN and checking cash availability before dispensing the requested amount, PIN Change, Balance Inquiry letting users check the balance of their linked bank account, and Mini Statement which allows users to view recent transactions if any.

Each of the sections in this article will be the title for each folder accompanied with an explanation for what the file does under it.

Controllers

// ATM

import java.util.Scanner;
import java.util.UUID;

public class ATM {
    private UUID id;
    private CashReserve cashReserve;
    private CashDispenserStrategy cashDispenserStrategy;

    public ATM() {
        cashReserve = new CashReserve();
        this.id = UUID.randomUUID();
    }

    // Method to set cash dispensing strategy
    public void setCashDispenserStrategy(CashDispenserStrategy cashDispenserStrategy) {
        this.cashDispenserStrategy = cashDispenserStrategy;
    }

    // Method to handle user requests after ATM card is inserted and PIN is verified
    public void startTransaction(AtmCard atmCard) {
        Scanner sc = new Scanner(System.in);
        Transaction transaction = new Transaction();
        if (!transaction.verifyPin(atmCard)) {
            System.out.println("Wrong PIN!!!");
            return;
        }
        while (true) {
            System.out.println("Choose an option: ");
            System.out.println(
                            "1: Withdraw cash\n" +
                            "2: Change Pin\n" +
                            "3: Check balance\n" +
                            "4: Get mini statement\n" +
                            "5: Exit");
            int option = sc.nextInt();
            boolean exit = false;
            switch (option) {
                case 1:
                    transaction.withdraw(cashReserve, cashDispenserStrategy);
                    break;
                case 2:
                    transaction.changePin(atmCard);
                    break;
                case 3:
                    transaction.checkBalance(atmCard);
                    break;
                case 4:
                    transaction.getMiniStatement(atmCard);
                    break;
                case 5:
                    System.out.println("Thank you for visiting!!!");
                    exit = true;
                    break;
                default:
                    System.out.println("Invalid option!!!");
            }
            if (exit) {
                break;
            }
        }
    }

    // Method to update data about cash reserve in ATM
    public void modifyCashReserve(CashReserve newCashReserve) {
        this.cashReserve.modifyCashReserve(newCashReserve);
        System.out.println("Cash reserve modified successfully!");
    }

    // Method to verify admin login
    public boolean adminLogin(String password) {
        if (password.equalsIgnoreCase("root")) {
            return true;
        }
        return false;
    }
}

Above, we define an ATM controller class. This class manages the core functionalities of the ATM. It creates a unique identifier for the ATM, stores the cash reserve information, and allows setting a strategy for dispensing cash. Additionally, it handles user transactions after successful card insertion and PIN verification. Users can withdraw cash, change PIN, check balance, get mini statements, and exit. Our class also provides functionalities for authorized admins to modify cash reserve and verify login, our admin login password being root.

// Transaction
import java.util.Scanner;

// Contains all facilities provided by ATM to users
public class Transaction {
    Scanner sc = new Scanner(System.in);

    // Method to withdraw cash from ATM
    public void withdraw(CashReserve cashReserve, CashDispenserStrategy cashDispenserStrategy) {
        System.out.println("Enter amount: ");
        int amount = sc.nextInt();
        // Contact bank to check user's balance
        // If not enough balance, cancel transaction
        // else
        CashAvailabilityDataResponseModel response = cashDispenserStrategy.getCashAvailability(cashReserve, amount);
        if (!response.isCashAvailable) {
            System.out.println("Cash not available!");
            return;
        }
        // Contact bank to deduct balance
        CashReserve cash = cashDispenserStrategy.dispenseCash(cashReserve, response.cashReserve);
        if (cash == null) {
            System.out.println("Something went wrong! Please contact XXXX");
            // Contact bank to initiate refund
        } else {
            System.out.println("Please collect cash!");
        }
    }

    // Method to change ATM card PIN
    public void changePin(AtmCard atmCard) {
        System.out.println("Enter new PIN: ");
        int newPin = sc.nextInt();
        // Contact bank to change PIN
        System.out.println("Pin changed successfully!");
    }

    // Method to check balance of bank account linked to given ATM card
    public void checkBalance(AtmCard atmCard) {
        // Contact bank to check balance
        System.out.println("Balance for account linked to card " + atmCard.getCardNumber() + " is YY");
    }

    // Method to get Mini-statement of bank account linked to given ATM card
    public void getMiniStatement(AtmCard atmCard) {
        // Contact bank to get mini-statement
        System.out.println("Mini-statement: ###########");
    }

    // Method to verify ATM card PIN
    public boolean verifyPin(AtmCard atmCard) {
        System.out.println("Please enter PIN: ");
        int pin = sc.nextInt();

        // PIN verification process
        return atmCard.getBank().verifyAtmPin(atmCard.getCardNumber(), pin);
    }
}

Above, we define a class to handle various user transactions at the ATM. It relies on user input through a Scanner object. We then define the fuctionality, the user can;

withdraw an amount. We check the cash availability using the CashDispenserStrategy and deduct the amount from the cash reserve. If successful, cash is dispensed, otherwise we show an error message. Change their pin by entering a new PIN, which is then sent to the bank (simulated) for verification and update. Check their balance by retrieving the balance for the account linked to the inserted ATM card (simulated). Print a mini statement. Verify their pin through a method that prompts the user for a PIN and sends it to the ATM card’s bank (simulated) for verification. It returns true if the PIN is correct.

Enums

// Denomination
public enum Denomination {
    ONE, TWO, FIVE, TEN, TWENTY, FIFTY, HUNDRED
}

The code for Denomination.java defines an enum called Denomination. Enums are a special type in Java that represent a fixed set of named constants. In this case, the Denomination enum represents the different denominations of currency that the ATM could dispense.

The enum consists of five constants: ONE, TWO, FIVE, TEN, TWENTY, FIFTY, and HUNDRED. The constraints are the face values of the bills the ATM can provide (e.g., 100, 50, 20, 10, 5, 2, 1 units of currency).

// PaymentNetwork
public enum PaymentNetwork {
  VISA, MASTERCARD
}

Similar to the Denomination enum, this file represents the different supported payment networks that the ATM interacts with.

Models

// Bank
public class Bank {
  private String bankName;

  public Bank(String bankName) {
    this.bankName = bankName;
  }

  public boolean verifyAtmPin(String cardNumber, int pin) {
    // Pin verification logic here
    return true;
  }
}

Above, our Bank class offers a constructor that takes the bank name as input and initializes the bankName field. With an additional method called verifyAtmPin. This method simulates the process of verifying the PIN entered by the user at the ATM. It takes the card number and the entered PIN as arguments. Since this is a simulated solution, the verifyAtmPin function returns true no matter what.

// CashAvailabilityDataResponseModel
public class CashAvailabilityDataResponseModel {
    public boolean isCashAvailable;
    public CashReserve cashReserve;

    // Response returned by getCashAvailability method Cash dispensing strategy
    public CashAvailabilityDataResponseModel(boolean isCashAvailable, CashReserve cashReserve) {
        this.isCashAvailable = isCashAvailable;
        this.cashReserve = cashReserve;
    }
}

We have two member variables: isCashAvailable: a boolean flag that says whether the requested amount can be dispensed based on the current cash reserve in the ATM. cashReserve: an object of type CashReserve.

// CashReserve
import java.util.HashMap;

public class CashReserve {
    private HashMap<Denomination, Integer> cashReserveMap;

    public void modifyCashReserve(CashReserve newCashReserve) {
        newCashReserve.cashReserveMap.forEach((currency, count) -> {
            this.cashReserveMap.put(currency, this.cashReserveMap.get(currency) + count);
        });
    }

    public CashReserve() {
        cashReserveMap = new HashMap<>();
        for (Denomination denomination : Denomination.values()) {
            cashReserveMap.put(denomination, 0);
        }
    }

    public HashMap<Denomination, Integer> getCashReserveMap() {
        return cashReserveMap;
    }

    public void updateCash(Denomination denomination, Integer count) {
        cashReserveMap.put(denomination, count);
    }
}

Here, we define a class to manage the ATM’s cash reserve. The class uses a HashMap to store the amount of each denomination available in the ATM. We initilize a HashMap named cashReserveMap with all possible Denomination enums as keys and sets their initial values to 0 (representing no bills of that denomination). The modifyCashReserve method takes another CashReserve object as input and merges its cash reserve information with the current ATM’s reserve. Then it iterates through the input cashReserveMap and adds the count for each denomination to the existing count in the ATM’s cashReserveMap. Further down, the getCashReserveMap method returns a copy of the internal cashReserveMap, providing a read-only view of the current cash reserve. The updateCash method allows updating the count for a specific denomination. It takes a Denomination enum and an integer count as arguments and directly updates the corresponding value in the cashReserveMap. We’re also ensuring type safety and readability by using a HashMap with Denomination enums as keys.

// Currency
public class Currency {
    public static int getValue(Denomination denomination) {
        switch (denomination) {
            case FIFTY:
                return 50;
            case TWENTY:
                return 20;
            case TEN:
                return 10;
            case FIVE:
                return 5;
            case TWO:
                return 2;
            case ONE:
                return 1;
            default:
                throw new IllegalArgumentException("Denomination not supported!");
        }
    }
}

Up top, we’re using a static method to get the value associated with a given denomination. The getValue method is static, meaning it can be called without creating an instance of the Currency class. It takes a Denomination enum as input and uses a switch statement to return the corresponding integer value. For example, if the input is Denomination.FIFTY, it returns 50.

Models > AtmCard

// AtmCard
import java.time.LocalDate;

public class AtmCard {
    private String cardNumber;
    private int CVV;
    private PaymentNetwork paymentNetwork;
    private String ownerName;
    private LocalDate expiryDate;
    private Bank bank;

    public AtmCard(String cardNumber, int CVV, String ownerName, LocalDate expiryDate) {
        this.cardNumber = cardNumber;
        this.CVV = CVV;
        this.ownerName = ownerName;
        this.expiryDate = expiryDate;
    }

    public void setPaymentNetwork(PaymentNetwork paymentNetwork) {
        this.paymentNetwork = paymentNetwork;
    }

    public void setBank(Bank bank) {
        this.bank = bank;
    }

    public Bank getBank() {
        return this.bank;
    }

    public String getCardNumber() {
        return cardNumber;
    }
}

For the above, we define a class named AtmCard that represents an ATM card. It stores information about the card including card number, CVV code, payment network (e.g., VISA, Mastercard), owner name, expiry date, and the associated bank. Then our class offers methods to set the payment network and bank after the card is created, as well as getter methods to access the card details.

// VisaCard
import java.time.LocalDate;

public class VisaCard extends AtmCard {
  public VisaCard(String cardNumber, int CVV, String ownerName, LocalDate expiryDate) {
    super(cardNumber, CVV, ownerName, expiryDate);
    this.setBank(new Bank("Bank of America"));
    this.setPaymentNetwork(PaymentNetwork.VISA);
  }
}

Above, we define a class named VisaCard that inherits from the AtmCard class. It specifically represents a Bank of America card. We inherit the card details (number, CVV, owner name, expiry) from the AtmCard class and pre-configure details for Bank of America cards.

// MasterCard
import java.time.LocalDate;

public class MasterCard extends AtmCard {
  public MasterCard(String cardNumber, int CVV, String ownerName, LocalDate expiryDate) {
    super(cardNumber, CVV, ownerName, expiryDate);
    this.setBank(new Bank("American Express"));
    this.setPaymentNetwork(PaymentNetwork.AMEX);
  }
}

Above, we define a class named MasterCard that inherits from the AtmCard class similar to VisaCard. It specifically represents a American Express card.

Following the same logic as VisaCard, this class simplifies creating American Express cards by: Inheriting card details from the AtmCard class. Setting the paymentNetwork to PaymentNetwork.AMEX and bank to a new Bank object with the name “American Express” in the constructor. This pre-configures these details for this card.

Strategies

// CashDispenserStrategy
public interface CashDispenserStrategy {
    CashReserve dispenseCash(CashReserve cashReserve, CashReserve cashRequest);

    CashAvailabilityDataResponseModel getCashAvailability(CashReserve cashReserve, int amount);
}

For our file above, we define an interface called CashDispenserStrategy. This interface outlines the functionalities required for any strategy that dispenses cash from the ATM. For any class implementing this interface, they must use the dispenseCash and getCashAvailability methods. One to withdraw cash and the other to calculate the current currency amount.

// SimpleCashDispenseStrategy
import java.util.Map;

// Works for our current denomination
public class SimpleCashDispenseStrategy implements CashDispenserStrategy {
    @Override
    public CashReserve dispenseCash(CashReserve cashReserve, CashReserve cashRequest) {
        cashRequest.getCashReserveMap().forEach((key, value) -> {
            cashReserve.getCashReserveMap().put(key, cashReserve.getCashReserveMap().get(key) - value);
        });
        return cashRequest;
    }

    @Override
    public CashAvailabilityDataResponseModel getCashAvailability(CashReserve cashReserve, int amount) {
        boolean isCashAvailable = false;
        CashReserve result = new CashReserve();

        for (Map.Entry<Denomination, Integer> entry : cashReserve.getCashReserveMap().entrySet()) {
            Denomination denomination = entry.getKey();
            Integer count = entry.getValue();

            while (count > 0 && amount > 0 && Currency.getValue(denomination) <= amount) {
                amount -= Currency.getValue(denomination);
                count -= 1;
                if (result.getCashReserveMap().containsKey(denomination)) {
                    result.getCashReserveMap().put(denomination, result.getCashReserveMap().get(denomination) + 1);
                } else {
                    result.getCashReserveMap().put(denomination, 1);
                }
            }
        }

        if (amount == 0) isCashAvailable = true;

        return new CashAvailabilityDataResponseModel(isCashAvailable, result);
    }
}

And before our Main java file to run the app, we have the class SimpleCashDispenseStrategy that implements the CashDispenserStrategy interface. This strategy focuses on dispensing cash using the available denominations in the ATM.

The dispenseCash method subtracts the requested cash amount (represented by another CashReserve object) directly from the current ATM’s cash reserve. The getCashAvailability method checks if the ATM can dispense a requested withdrawal amount. It iterates through the available denominations and tries to build a combination of bills that fulfills the withdrawal amount.

It uses a loop to keep subtracting the denomination value from the requested amount as long as there are enough bills of that denomination and the remaining amount is greater than zero. If a sufficient combination is found (meaning the remaining amount becomes zero), it marks cash as available and stores the dispensed bill combination in a temporary CashReserve object. Then in the end, it returns a response object indicating cash availability and the potential dispensed cash composition (if available).

Main

// Main
import java.time.LocalDate;
import java.util.Scanner;

public class Main {

    // Method to simulate insertion of cash in ATM
    public static void handleModifyCashReserve(ATM atm) {
        Scanner sc = new Scanner(System.in);
        System.out.println("Enter details to modify cash reserve... Enter q to exit: ");
        CashReserve request = new CashReserve();
        while (true) {
            System.out.print("Enter denomination type (TWO_THOUSAND, THOUSAND, FIVE_HUNDRED, HUNDRED, FIFTY) or \"q\" to exit: ");
            String input = sc.next();
            if (input.equalsIgnoreCase("q")) {
                break;
            }
            Denomination denomination = Denomination.valueOf(input.toUpperCase());
            System.out.println("Enter count: ");
            Integer count = sc.nextInt();
            try {
                request.updateCash(denomination, count);
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
        atm.modifyCashReserve(request);
    }

    // Running main method means ATM is up and running
    public static void main(String[] args) {
        ATM atm = new ATM();
        atm.setCashDispenserStrategy(new SimpleCashDispenseStrategy());
        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.println("1: User login\n2: Admin login");
            int loginType = sc.nextInt();
            switch (loginType) {
                case 1:
                    // ATM card insertion stage
                    AtmCard dummyAtmCard = new MasterCard("1234321", 999, "Anuva", LocalDate.now());
                    atm.startTransaction(dummyAtmCard);
                    break;
                case 2:
                    System.out.print("Enter admin password: ");
                    String input = sc.next();
                    if (atm.adminLogin(input)) {
                        System.out.println("Admin Logged in successfully!");
                        handleModifyCashReserve(atm);
                    } else {
                        System.out.println("Invalid password!");
                    }
                    break;
                default:
                    System.out.println("Invalid option!");
                    break;
            }
        }

    }
}

// Our password for admin is: "root"

For our entry point to the run the program, we define two main functionalities: user login and admin login. For user login, we simulate inserting a pre-defined ATM card and starting a transaction. Admin login requires a password, it’s . If successful, it allows modifying the cash reserve in the ATM. This modification involves specifying the bill denomination (e.g., two thousand rupees) and the number of bills to be added. The program has error handling to catch exceptions during cash reserve updates.

Conclusion

This project covered alot of the functionality of building an ATM in Java. We used built and instantiated interfaces using their methods to simulate an ATM. Hope you enjoyed the article and it helped you. Until then, have a good one!

Resources