system-design49 min read

How to Design an ATM System — A Senior+ Guide | Ayodhyya

How to Design an ATM System

A Senior+ Guide — Building cash dispensing, card processing, transaction management, and fleet monitoring for 3M+ ATMs worldwide

Published: July 14, 2026  |  Reading Time: ~45 min  |  By Ayodhyya

1. Introduction — 3M+ ATMs Globally

The Automated Teller Machine remains one of the most critical pieces of financial infrastructure ever deployed. With over 3.5 million ATMs operating worldwide and processing more than 100 billion transactions annually, these machines represent the frontline of banking — the physical touchpoint where digital money becomes tangible cash. From the streets of Mumbai to the motorways of Germany, ATMs dispense an estimated $8 trillion in currency every single year.

Designing an ATM system is not merely a software engineering exercise. It is a multi-disciplinary challenge that spans embedded systems, cryptographic security, financial protocols, real-time hardware control, distributed computing, and regulatory compliance. A single ATM interacts with card readers, cash dispensers, receipt printers, encrypting PIN pads, network modems, cameras, and sensors — all while communicating with bank backends using decades-old financial message formats like ISO 8583.

Consider the complexity involved: when a customer inserts a chip card, the ATM must authenticate the card through EMV protocol, verify the PIN through a Hardware Security Module (HSM), check account balances across potentially different banking systems, dispense the correct denominations from mechanical cassettes, print a receipt, update the transaction journal, reconcile cash levels, and complete the entire flow within 30 seconds — all while maintaining an unbroken chain of cryptographic audit.

The modern ATM ecosystem has evolved far beyond simple cash dispensing. Today's machines support contactless NFC payments, biometric authentication, video banking, bill payments, check deposits with image capture, cardless withdrawals via QR codes, and even cryptocurrency transactions. This evolution has dramatically increased the complexity of ATM system design while raising the bar for security, reliability, and performance.

This guide provides a comprehensive deep-dive into designing a production-grade ATM system. We will cover everything from hardware abstractions and session state machines to EMV chip processing, cash cassette denomination optimization, anti-skimming security, multi-bank interoperability, and fleet-wide cash management forecasting. Whether you are preparing for a senior system design interview or architecting a real ATM platform, this article gives you the full picture.

Why ATM System Design Matters in Interviews: ATM design tests your ability to handle real-time constraints, stateful sessions, hardware integration, cryptographic security, distributed transactions, and financial compliance — all in a single system. It is a favorite topic for senior and staff-level interviews at fintech companies, banks, and payment processors.

2. Functional & Non-Functional Requirements

Functional Requirements

#RequirementDescriptionPriority
FR-01Cash WithdrawalUser withdraws cash from linked account with denomination selectionP0 — Critical
FR-02Cash DepositUser deposits cash or checks via envelope-free deposit with image captureP0 — Critical
FR-03Balance InquiryUser checks account balance and available balanceP0 — Critical
FR-04PIN ChangeUser changes PIN with old PIN verificationP1 — High
FR-05Fund TransferTransfer between linked accounts at same bankP1 — High
FR-06Mini StatementPrint last 10 transactions on receiptP1 — High
FR-07Cardless WithdrawalQR code or one-time code based cash withdrawalP2 — Medium
FR-08Bill PaymentUtility bill payment through ATMP2 — Medium
FR-09Multi-LanguageSupport 3+ languages per regionP1 — High
FR-10Receipt PrintingPrint or email transaction receiptP1 — High

Non-Functional Requirements

#RequirementTargetJustification
NFR-01Availability99.95% uptimeATMs must be available 24/7; planned maintenance windows
NFR-02Latency< 30 seconds end-to-endCustomer patience threshold; card timeout limits
NFR-03Throughput100+ transactions/second per regionPeak lunch-hour and month-end salary volumes
NFR-04SecurityPCI DSS Level 1 complianceMandatory for card processing; zero-tolerance for breaches
NFR-05Durability15-year hardware lifecycleATMs are deployed for extended periods
NFR-06Fault ToleranceGraceful degradationPartial functionality if one module fails
NFR-07Audit Trail7-year retentionFinancial regulatory compliance (SOX, PCI, local banking laws)
NFR-08Offline ModeStore-and-forward for network outagesATMs in remote areas face connectivity issues

Core ATM Operations Flow

The fundamental ATM interaction follows a strict sequence: Card Insert → Card Read → PIN Entry → PIN Verify → Transaction Selection → Amount Entry → Authorization → Dispense/Process → Receipt → Card Return. Each step has a timeout, error handler, and audit event. The entire session is managed as a finite state machine with well-defined transitions and guard conditions.

3. Capacity Estimation

Back-of-Envelope Calculations

Let us estimate the system requirements for a mid-size bank operating 50,000 ATMs across a country.

MetricCalculationResult
Total ATMsDirect deployment50,000
Avg transactions/ATM/dayIndustry average for urban areas250
Daily transactions50,000 × 25012,500,000
Peak transactions/hour15% of daily in 1 hour (lunch peak)1,875,000
Peak transactions/second1,875,000 / 3600~520 TPS
Avg withdrawal amountRegional average$200
Daily cash dispensed12.5M × $200 (assuming 80% withdrawals)$2 billion
Avg cash per ATM$40,000 loaded$2 billion total float
Data per transactionISO 8583 message ~1KB + audit ~0.5KB1.5 KB
Daily storage12.5M × 1.5 KB~19 GB/day
Annual storage19 GB × 365~7 TB/year
Network bandwidth per ATM50 messages/day × 2 KB average~100 KB/day (very low)

These numbers reveal that while individual ATMs generate modest data volumes, the aggregate fleet creates significant write throughput requirements. The system must handle 500+ TPS at peak with strict ordering guarantees for transaction sequences on individual ATMs. The storage requirement of 7 TB per year demands a tiered storage strategy with hot, warm, and cold data partitions.

Key Insight: The real challenge is not raw throughput — it is the consistency requirement. A dispense mismatch (ATM shows success but cash not taken, or cash dispensed but account not debited) can cost real money. The system must guarantee exactly-once transaction semantics despite network partitions, hardware failures, and concurrent access.

4. Data Model

The data model for an ATM system spans multiple bounded contexts: terminal management, card management, account management, transaction processing, and cash inventory. Below we define the core entities and their relationships.

erDiagram ATM ||--o{ ATM_SESSION : hosts ATM_SESSION ||--o{ TRANSACTION : contains CARD ||--o{ ATM_SESSION : authenticates ACCOUNT ||--o{ TRANSACTION : debits_or_credits ATM ||--o{ CASH_CASSETTE : contains ATM ||--o{ ATM_AUDIT_LOG : generates TRANSACTION }|--|| TRANSACTION_TYPE : classified_as CARD }|--|| CARD_STATUS : has ATM }|--|| ATM_STATUS : has ATM { string atm_id PK string location_code string bank_code string ip_address decimal latitude decimal longitude string status datetime last_heartbeat int software_version } ATM_SESSION { string session_id PK string atm_id FK string card_token string session_state datetime started_at datetime ended_at string terminal_trace } CARD { string card_token PK string card_number_hash string card_number_encrypted string cardholder_name string card_status datetime issued_date datetime expiry_date string card_type string emv_aid } ACCOUNT { string account_id PK string account_number_hash string account_type decimal balance decimal available_balance string currency_code string status datetime last_updated } TRANSACTION { string transaction_id PK string session_id FK string account_id FK string atm_id FK string transaction_type decimal amount string currency_code string status string auth_code string reversal_flag datetime created_at datetime settled_at string network_trace } CASH_CASSETTE { string cassette_id PK string atm_id FK int denomination int capacity int current_count string cassette_type datetime last_replenished } TRANSACTION_TYPE { string type_code PK string description boolean requires_auth boolean reversible } CARD_STATUS { string status_code PK string description } ATM_STATUS { string status_code PK string description } ATM_AUDIT_LOG { string log_id PK string atm_id FK string session_id FK string event_type string event_detail datetime event_time string severity }

Entity Descriptions

ATM Entity

The ATM entity represents a physical terminal deployed at a location. It includes network configuration, hardware specifications, and operational status. Each ATM is identified by a unique atm_id (typically a 6-8 digit identifier printed on the machine). The location_code enables geo-based routing and cash management. The last_heartbeat field tracks connectivity status — if no heartbeat is received within the configured interval (typically 60 seconds), the ATM is marked as offline.

Transaction Entity

Every financial operation at an ATM generates a transaction record. The transaction entity includes references to the session, account, and ATM, along with the amount, authorization code, and settlement status. The reversal_flag tracks whether this transaction has been reversed. The network_trace field stores the ISO 8583 trace number for reconciliation with the payment network. Transactions progress through states: PENDING → AUTHORIZED → COMPLETED → SETTLED or PENDING → DECLINED / REVERSED.

5. API Design

The ATM system exposes a set of internal APIs consumed by the ATM terminal firmware and the bank's backend systems. These APIs follow REST conventions for management operations and use ISO 8583 message formats for real-time transaction processing.

Terminal APIs (ATM → Backend)

EndpointMethodDescriptionKey Parameters
/api/v1/terminal/heartbeatPOSTPeriodic status signal from ATMatm_id, status, cassette_levels[], software_version
/api/v1/card/insertPOSTCard inserted event, begin sessionatm_id, card_track2_encrypted, reader_type
/api/v1/card/pin/verifyPOSTSubmit encrypted PIN block for verificationsession_id, encrypted_pin_block, pin_length
/api/v1/transaction/withdrawPOSTCash withdrawal authorization requestsession_id, account_id, amount, denomination_prefs
/api/v1/transaction/depositPOSTCash or check depositsession_id, account_id, deposit_type, images[]
/api/v1/transaction/balanceGETAccount balance inquirysession_id, account_id
/api/v1/transaction/statementGETMini statement (last N transactions)session_id, account_id, count
/api/v1/transaction/completePOSTConfirm transaction completion (cash taken)session_id, transaction_id, dispense_status
/api/v1/card/ejectPOSTCard returned to customer, session endsession_id, reason

Management APIs (Backend → ATM Fleet)

EndpointMethodDescription
/api/v1/fleet/statusGETGet status of all ATMs or filtered subset
/api/v1/fleet/{atm_id}/configPUTPush configuration update to specific ATM
/api/v1/fleet/{atm_id}/rebootPOSTRemote reboot command
/api/v1/fleet/{atm_id}/cash-forecastGETGet cash replenishment forecast
/api/v1/fleet/alertsGETActive alerts across the fleet
sequenceDiagram participant ATM as ATM Terminal participant GW as API Gateway participant AUTH as Auth Service participant TXN as Transaction Service participant HSM as HSM Cluster participant DB as Database ATM->>GW: POST /card/insert (card_track2_encrypted) GW->>AUTH: Validate card & create session AUTH->>DB: INSERT session (CARD_INSERTED) AUTH-->>ATM: session_id, prompt for PIN ATM->>GW: POST /card/pin/verify (encrypted_pin_block) GW->>AUTH: Forward PIN block AUTH->>HSM: Decrypt & verify PIN HSM-->>AUTH: PIN verified (offset match) AUTH->>DB: UPDATE session (PIN_VERIFIED) AUTH-->>ATM: PIN valid, show menu ATM->>GW: POST /transaction/withdraw (amount=200) GW->>TXN: Process withdrawal TXN->>DB: Check balance TXN->>TXN: Authorize (hold funds) TXN->>DB: INSERT transaction (AUTHORIZED) TXN-->>ATM: Authorized, dispense cash ATM->>GW: POST /transaction/complete (dispense_status=OK) GW->>TXN: Confirm completion TXN->>DB: UPDATE transaction (COMPLETED) TXN-->>ATM: Done, eject card

6. High-Level Architecture

The ATM system architecture consists of four major layers: the ATM Terminal (edge), the ATM Switch (processing), the Bank Backend (core banking), and the Payment Network (interbank). Each layer has distinct responsibilities, scaling characteristics, and failure modes.

graph TB subgraph ATM_Fleet["ATM Fleet (Edge)"] ATM1["ATM #1
Branch Location"] ATM2["ATM #2
Shopping Mall"] ATM3["ATM #3
Airport"] ATM4["ATM #N
Remote Site"] end subgraph Network["Network Layer"] VPN["VPN Tunnel
IPSec / MPLS"] LB["Load Balancer
L4 / L7"] FW["Firewall
PCI DSS"] end subgraph Switch["ATM Switch / Processing Layer"] GW["API Gateway
Rate Limiting"] SESS["Session Manager
State Machine"] PINV["PIN Verification
Service"] TXN["Transaction
Processor"] DISP["Cash Dispensing
Controller"] RECV["Reversal
Engine"] AUDIT["Audit
Service"] end subgraph CoreBank["Core Banking Layer"] ACCT["Account
Service"] LEDGER["General
Ledger"] CUST["Customer
Service"] CARD_SVC["Card
Management"] end subgraph PaymentNet["Payment Network"] NET1["VisaNet"] NET2["Mastercard Banknet"] NET3["Regional Switch
e.g. NPCI / LINK"] NET4["PIN Network
PLUS / Cirrus"] end subgraph Infra["Infrastructure"] DB["Primary DB
PostgreSQL"] DB_REP["Read Replicas"] CACHE["Redis Cache
Balance + Config"] MQ["Message Queue
Kafka / RabbitMQ"] HSM["HSM Cluster
Thales / Utimaco"] VAULT["Encrypted
Key Vault"] MON["Monitoring
Prometheus + Grafana"] end ATM1 & ATM2 & ATM3 & ATM4 --> VPN VPN --> FW --> LB LB --> GW GW --> SESS & TXN & DISP & RECV & AUDIT SESS --> PINV PINV --> HSM TXN --> ACCT & LEDGER & CARD_SVC TXN --> NET1 & NET2 & NET3 & NET4 ACCT --> DB DB --> DB_REP TXN --> MQ SESS --> CACHE AUDIT --> DB MON --> ATM_Fleet & Switch & CoreBank

Architecture Principles

  • Defense in Depth: Every layer has its own authentication, encryption, and validation. A compromised ATM cannot directly access the core banking system.
  • Idempotency: All transaction APIs are idempotent. Retrying a failed dispense authorization will not result in duplicate debits.
  • Event Sourcing: The transaction processor uses an event-sourced model. Every state change is recorded as an immutable event, enabling full audit trail and deterministic replay.
  • Circuit Breaking: If the core banking system is unreachable, the ATM switch enters degraded mode — allowing balance inquiries from cache and queueing withdrawal requests for later processing.
  • Hot Standby: The ATM switch runs in active-active configuration across two data centers. If one fails, the other takes over within seconds via health-check-based failover.

7. ATM Terminal Hardware

Understanding ATM hardware is essential for software design because the software must gracefully handle hardware events, failures, and constraints. Each component communicates with the terminal controller through standardized interfaces.

graph LR subgraph ATM_Hardware["ATM Terminal"] CC["Card Reader
EMV + Swipe + NFC"] EPP["Encrypting PIN Pad
DES/Triple DES"] CD["Cash Dispenser
4-8 Cassettes"] RP["Receipt Printer
Thermal"] DP["Deposit Module
Check Scanner + Cash Acceptor"] DISPLAY["Display Screen
LCD / TFT"] CAM["Camera
PIN + Facial"] SENS["Sensors
Door, Temperature, Vibration"] NETM["Network Module
4G + Ethernet"] UPS["UPS Battery
30 min backup"] end CC --> TC["Terminal Controller
X86 / ARM"] EPP --> TC CD --> TC RP --> TC DP --> TC DISPLAY --> TC CAM --> TC SENS --> TC NETM --> TC UPS --> TC
ComponentInterfaceData RateFailure ModeSoftware Impact
Card Reader (Motorized)RS-232 / USB9600 bpsCard jam, read failureEject card, log event, alert fleet mgmt
Card Reader (Contactless)ISO 14443 / NFC424 kbpsTag read failure, interferenceFallback to swipe/chip, retry 3x
Encrypting PIN PadInternal bus (encrypted)Tamper detected, self-destructShut down all PIN operations immediately
Cash DispenserCCDM protocol9600 bpsShutter jam, pick sensor, note sensorAbort dispense, void transaction, reverse hold
Receipt PrinterESC/POS over RS-23219200 bpsPaper out, print head failureEmail receipt fallback, log warning
Deposit ModuleProprietary + image SDKUSB 2.0Scanner jam, image quality failureReject deposit, ask user to retry
PIN CameraUSB / IP camera720p streamLens obstruction, failureLog alert, continue transaction (non-critical)
Hardware Constraint: ATM terminals typically run on low-power embedded systems with 4-8 GB RAM and limited storage. The terminal software must be lightweight, memory-safe, and capable of operating during intermittent network connectivity. Most modern ATMs run a hardened Linux distribution or Windows IoT with a custom application layer.

8. Session State Machine

The ATM session is the most critical real-time concept in the system. Every customer interaction is a session with well-defined states, transitions, timeouts, and compensating actions. The state machine ensures that the ATM always knows exactly what step the customer is on and what actions are valid.

stateDiagram-v2 [*] --> IDLE: Machine Ready IDLE --> CARD_INSERTED: Card detected by reader CARD_INSERTED --> PIN_ENTRY: Track2 read OK CARD_INSERTED --> CARD_EJECTED: Read failure (3 retries) PIN_ENTRY --> PIN_VERIFIED: PIN correct PIN_ENTRY --> PIN_FAILED: PIN incorrect PIN_FAILED --> PIN_ENTRY: Retries remaining PIN_FAILED --> CARD_EJECTED: Max retries exceeded PIN_VERIFIED --> MENU_DISPLAYED: Show transaction menu MENU_DISPLAYED --> AMOUNT_ENTRY: User selects withdrawal MENU_DISPLAYED --> BALANCE_INQUIRY: User selects balance MENU_DISPLAYED --> TRANSFER_MENU: User selects transfer MENU_DISPLAYED --> DEPOSIT_FLOW: User selects deposit AMOUNT_ENTRY --> AUTHORIZING: Amount submitted AUTHORIZING --> DISPENSING: Authorization approved AUTHORIZING --> DECLINED: Insufficient funds / limit exceeded AUTHORIZING --> TIMEOUT: Backend unreachable (30s) DISPENSING --> CASH_PRESENTED: Dispenser confirms notes CASH_PRESENTED --> COMPLETE: Cash taken sensor triggered CASH_PRESENTED --> CASH_RETURNED: Timeout (30s) no take DECLINED --> MENU_DISPLAYED: Show error, return to menu TIMEOUT --> MENU_DISPLAYED: Show error, return to menu BALANCE_INQUIRY --> MENU_DISPLAYED: Show balance DEPOSIT_FLOW --> DEPOSIT_ACCEPTING: Insert cash/checks DEPOSIT_ACCEPTING --> DEPOSIT_COUNTED: Module confirms count DEPOSIT_COUNTED --> MENU_DISPLAYED: Credit account TRANSFER_MENU --> AUTHORIZING: Transfer details entered COMPLETE --> CARD_EJECTING: Session ending CASH_RETURNED --> CARD_EJECTING: Session ending CARD_EJECTING --> CARD_TAKEN: Customer takes card CARD_TAKEN --> IDLE: Session closed CARD_EJECTED --> CARD_TAKEN: Card returned after error CARD_TAKEN --> IDLE: Session closed IDLE --> IDLE: Timeout on idle (reset)

Session Timeout Configuration

StateTimeoutAction on Timeout
IDLENone (infinite)Return to screensaver
CARD_INSERTED10 secondsEject card, log event
PIN_ENTRY30 secondsEject card, log event
MENU_DISPLAYED60 secondsEject card, cancel session
AMOUNT_ENTRY30 secondsReturn to menu
AUTHORIZING30 secondsDecline, return to menu
DISPENSING15 secondsHardware retry, then void
CASH_PRESENTED30 secondsRetract cash, reverse transaction
DEPOSIT_ACCEPTING60 secondsReturn items, cancel deposit
CARD_EJECTING10 secondsRetract and retain card

The state machine is implemented as a deterministic finite automaton with guarded transitions. Each transition is validated against the current state, session data, and hardware status before execution. Invalid transitions are logged as security events because they may indicate tampering or firmware exploitation.

9. Card Processing & EMV

EMV (Europay, Mastercard, Visa) is the global standard for chip-based credit and debit card transactions. Understanding the EMV flow is essential because it dictates how the ATM authenticates the card and how the transaction is cryptographically secured end-to-end.

EMV Transaction Flow at ATM

sequenceDiagram participant C as Card (Chip) participant R as Card Reader participant T as Terminal participant H as HSM participant N as Network (Visa/MC) participant I as Issuer Bank C->>R: Power on (ICC contact) R->>T: Card detected, ATR received Note over T: Application Selection T->>C: SELECT PPSE (Payment System Environment) C-->>T: List of available applications (AIDs) T->>C: SELECT application (e.g., Visa Debit) C-->>T: Application data, AIP, AFL Note over T: Offline Data Authentication T->>C: GET PROCESSING OPTIONS C-->>T: TDOL, CVM list, terminal capabilities T->>C: READ RECORD (per AFL entries) C-->>T: Track 2 equivalent data, cardholder name, expiry Note over T: PIN Verification (CVM) alt Online PIN T->>H: Encrypted PIN block + PAN H-->>T: PIN verification result else Offline PIN T->>C: VERIFY PIN (plaintext PIN sent to chip) C-->>T: PIN verified (Y/N) end Note over T: Transaction Authorization T->>T: Generate ARQC (Auth Request Cryptogram) T->>N: Authorization request (ARQC + transaction data) N->>I: Forward to issuer I->>I: Check balance, fraud rules, limits I-->>N: Approval / Decline + ARPC N-->>T: Authorization response alt Approved T->>C: TRANSACTION LOG RECORD (ARPC verify) C-->>T: TC (Transaction Certificate) - card's proof Note over T: Dispense cash / complete transaction else Declined Note over T: Show decline, eject card end

Card Interface Types

InterfaceStandardDataSecurityUsage at ATM
EMV Chip (Contact)ISO 7816Full card data, cryptogramsHighest — dynamic auth per txnPrimary method, ~85% of transactions
Magnetic StripeISO 7811Static Track 1 + Track 2Low — skimmable, no dynamic authFallback only, being phased out
Contactless NFCISO 14443Same as chip via radioSame as chip, limited amount offlineGrowing — tap-to-withdraw in some markets

The ATM terminal must support all three interfaces while enforcing progressive security. If an EMV chip is present, it must be used. Magnetic stripe is only permitted as fallback when the chip reader fails. Contactless transactions may have lower per-transaction limits set by the issuer.

EMV Key Insight: Every EMV transaction generates a unique cryptogram (ARQC) using a secret key shared between the card and the issuer. This means that even if an attacker captures the complete transaction data, they cannot replay it for another transaction. This is the fundamental security advantage over magnetic stripe cards.

10. Cash Dispensing Logic

Cash dispensing is the core function of an ATM and involves complex hardware-software coordination. A typical ATM contains 4-8 cassettes, each loaded with a specific denomination. The dispensing algorithm must select the optimal combination of cassettes to fulfill the requested amount while respecting cassette levels, note quality, and customer preferences.

graph TD A["Withdrawal Request
Amount: $370"] --> B{"Denomination
Optimization"} B --> C["Check Cassette Levels"] C --> D{"Sufficient
Cash?"} D -->|Yes| E["Calculate Dispense Plan"] D -->|No| F["Try Alternative
Denominations"] F --> G{"Alternative
Available?"} G -->|Yes| E G -->|No| H["Partial Dispense
or Decline"] E --> I["Select Cassettes
200x1 + 100x1 + 50x1 + 20x1 + 10x2"] I --> J["Send Dispense
Command to Hardware"] J --> K{"Dispense
Successful?"} K -->|Yes| L["Count Verification
Pick Sensors"] K -->|No| M["Retry / Jam
Recovery"] L --> N{"Count
Matches?"} N -->|Yes| O["Present Cash
to Customer"] N -->|No| P["Retract &
Void Transaction"] M --> Q{"Retries
Exceeded?"} Q -->|No| J Q -->|Yes| P O --> R["Wait for
Take Sensor"] R --> S["Complete"] P --> T["Reverse Authorization
Log Discrepancy"]

Denomination Optimization Algorithm

public class DenominationOptimizer
{
    private readonly Dictionary<int, int> _cassetteLevels;

    public DenominationOptimizer(Dictionary<int, int> cassetteLevels)
    {
        _cassetteLevels = cassetteLevels;
    }

    public DispensePlan? Calculate(int amount)
    {
        var denominations = _cassetteLevels.Keys.OrderByDescending(d => d).ToList();
        var plan = new DispensePlan();
        int remaining = amount;

        foreach (var denom in denominations)
        {
            if (remaining <= 0) break;
            if (!_cassetteLevels.ContainsKey(denom)) continue;

            int available = _cassetteLevels[denom];
            int needed = remaining / denom;
            int toDispense = Math.Min(needed, available);

            if (toDispense > 0)
            {
                plan.AddNote(denom, toDispense);
                remaining -= denom * toDispense;
            }
        }

        if (remaining > 0)
        {
            // Cannot fulfill exact amount
            // Try lower denominations or partial dispense
            return TryPartialDispense(amount, denominations);
        }

        // Validate total note count does not exceed hardware limit
        if (plan.TotalNotes > 40) // Typical ATM limit per dispense
            return TryAlternativeSplit(amount);

        return plan;
    }

    private DispensePlan? TryPartialDispense(int amount, List<int> denominations)
    {
        // Attempt to dispense the largest amount possible
        // using available denominations
        var plan = new DispensePlan();
        int dispensed = 0;

        foreach (var denom in denominations)
        {
            if (!_cassetteLevels.ContainsKey(denom)) continue;
            int available = _cassetteLevels[denom];
            int maxFromThis = Math.Min(available, (amount - dispensed) / denom);
            if (maxFromThis > 0)
            {
                plan.AddNote(denom, maxFromThis);
                dispensed += denom * maxFromThis;
            }
        }

        return dispensed > 0 ? plan : null;
    }

    private DispensePlan? TryAlternativeSplit(int amount, List<int> denominations)
    {
        // Re-run with constraint on max notes per cassette
        // to spread across more cassettes
        return Calculate(amount);
    }
}

public class DispensePlan
{
    public List<(int denomination, int count)> Notes { get; } = new();
    public int TotalNotes => Notes.Sum(n => n.count);
    public int TotalAmount => Notes.Sum(n => n.denomination * n.count);

    public void AddNote(int denomination, int count)
    {
        Notes.Add((denomination, count));
    }
}

Cassette Management

CassettePositionCapacityLow ThresholdRefill Trigger
Cassette 1Top2,000 notes ($20)200 notesBelow 200 → alert CIT crew
Cassette 2Upper-Mid2,000 notes ($50)150 notesBelow 150 → alert CIT crew
Cassette 3Lower-Mid2,000 notes ($100)100 notesBelow 100 → alert CIT crew
Cassette 4Bottom1,000 notes ($10)100 notesBelow 100 → alert CIT crew
Deposit CassetteSeparate5,000 notes (mixed)Full → CIT pickup needed

Jam recovery is a critical hardware-software interaction. When a pick sensor detects a jam, the dispenser attempts up to 3 automatic retries. If retries fail, the notes are retracted into a secure reject bin, and the transaction is voided. The customer is never charged for a failed dispense. The reject bin is a separate physical compartment that CIT (Cash-In-Transit) personnel clear during replenishment visits.

11. PIN Verification

PIN verification is the most security-sensitive operation in the ATM system. The PIN never travels in plaintext beyond the Encrypting PIN Pad (EPP). The verification process uses Hardware Security Modules (HSMs) that are tamper-resistant cryptographic processors.

graph LR subgraph ATM["ATM Terminal"] EPP["Encrypting PIN Pad"] TR["Terminal Runtime"] end subgraph Backend["Bank Backend"] PIN_SVC["PIN Service"] subgraph HSM_Cluster["HSM Cluster"] HSM1["HSM Primary"] HSM2["HSM Secondary"] end VAULT["Key Vault"] end EPP -->|"3DES Encrypted
PIN Block (ISO 9564)"| TR TR -->|"Forward PIN Block
(never decrypted at terminal)"| PIN_SVC PIN_SVC -->|"PIN Block +
Zone Key Index"| HSM1 HSM1 -->|"Decrypt PIN Block
using Zone PIN Key"| HSM1 HSM1 -->|"Compare PIN offset
or PIN reference"| HSM1 HSM1 -->|"Result: VALID / INVALID"| PIN_SVC VAULT -->|"Key Management
Key Injection"| HSM_Cluster

PIN Block Formats

FormatStandardStructureUsage
ISO 9564 Format 0ISO 9564-1XOR of PIN (padded) + PAN (truncated)Legacy ATMs, most common globally
ISO 9564 Format 1ISO 9564-1Random key + encrypted PINHigher security environments
ISO 9564 Format 3ISO 9564-1Random key + encrypted (PIN length + PIN)Recommended for new deployments
ISO 9564 Format 4ISO 9564-1AES-128 encrypted PIN blockFuture-proof, AES-based

PIN Verification Methods

  1. Online PIN Verification: The encrypted PIN block is sent to the bank's HSM via the network. The HSM decrypts it using the zone PIN key (ZPK), extracts the plaintext PIN, and compares it against the PIN reference stored in the card management system. This is the most common method and provides real-time verification.
  2. Offline PIN Verification (Chip PIN): For EMV transactions, the PIN is sent directly to the card's chip processor. The chip compares the PIN internally and returns a simple yes/no result. The PIN never leaves the card. This is used for contactless transactions and offline-capable terminals.
  3. PIN Offset Verification: Instead of storing the actual PIN, the bank stores a PIN offset — a value derived from the PIN and the card's PAN. The HSM calculates the expected offset and compares it with the offset stored in the card's magnetic stripe or chip. This provides an additional layer of indirection.
public class PinVerificationService
{
    private readonly IHsmClient _hsmClient;
    private readonly ICardRepository _cardRepository;

    public async Task<PinVerificationResult> VerifyPinAsync(
        string sessionToken,
        byte[] encryptedPinBlock,
        string pan,
        PinBlockFormat format)
    {
        // Validate session is in PIN_ENTRY state
        var session = await _sessionStore.GetAsync(sessionToken);
        if (session?.State != SessionState.PIN_ENTRY)
            throw new InvalidStateException("Session not in PIN_ENTRY state");

        // Rate limiting: max 3 PIN attempts per session
        if (session.PinAttemptCount >= 3)
        {
            await _sessionStore.UpdateStateAsync(sessionToken, SessionState.BLOCKED);
            return PinVerificationResult.Blocked;
        }

        // Forward encrypted PIN block to HSM for verification
        var hsmRequest = new HsmPinVerifyRequest
        {
            EncryptedPinBlock = encryptedPinBlock,
            Pan = pan,
            PinBlockFormat = format,
            ZoneIndex = session.AtmZoneIndex
        };

        HsmPinVerifyResponse hsmResult;
        try
        {
            hsmResult = await _hsmClient.VerifyPinAsync(hsmRequest);
        }
        catch (HsmException ex)
        {
            _logger.LogError(ex, "HSM verification failed for session {Session}", sessionToken);
            return PinVerificationResult.SystemError;
        }

        // Update session state based on result
        if (hsmResult.IsValid)
        {
            await _sessionStore.IncrementPinAttemptsAsync(sessionToken);
            await _sessionStore.UpdateStateAsync(sessionToken, SessionState.PIN_VERIFIED);
            return PinVerificationResult.Valid;
        }
        else
        {
            await _sessionStore.IncrementPinAttemptsAsync(sessionToken);
            return PinVerificationResult.Invalid;
        }
    }
}

public enum PinVerificationResult
{
    Valid,
    Invalid,
    Blocked,
    SystemError
}

public enum PinBlockFormat
{
    ISO9564_0 = 0,
    ISO9564_1 = 1,
    ISO9564_3 = 3,
    ISO9564_4 = 4
}

12. Transaction Processing

Transaction processing in the ATM system involves three distinct phases: Authorization (can the customer perform this transaction?), Execution (dispense cash / update balances), and Settlement (finalize the financial posting). Each phase has different consistency and availability requirements.

graph TD A["Transaction Request"] --> B["Authorization Phase"] B --> B1["Validate Session State"] B1 --> B2["Check Account Balance"] B2 --> B3["Check Daily Limits"] B3 --> B4["Fraud Screening"] B4 --> B5{"Authorized?"} B5 -->|Yes| C["Execution Phase"] B5 -->|No| D["Decline Response"] C --> C1["Create Hold on Account"] C1 --> C2["Insert Transaction Record"] C2 --> C3["Issue Dispense Command"] C3 --> C4["Hardware Dispenses Cash"] C4 --> C5{"Dispense OK?"} C5 -->|Yes| E["Completion Phase"] C5 -->|No| F["Void & Reverse"] E --> E1["Customer Takes Cash"] E1 --> E2["Release Hold → Debit Account"] E2 --> E3["Update Transaction → COMPLETED"] E3 --> E4["Post to General Ledger"] E4 --> E5["Queue for Network Settlement"] E5 --> G["Transaction Done"] F --> F1["Reverse Hold on Account"] F1 --> F2["Log Reversal Event"] F2 --> H["Transaction Reversed"]

Transaction Lifecycle States

StateDescriptionAccount ImpactReversible?
PENDINGTransaction created, authorization in progressNoneYes (just delete)
AUTHORIZEDFunds held, awaiting executionBalance hold (available reduced)Yes (release hold)
COMPLETEDCash dispensed / deposit acceptedBalance debited / creditedYes (reversal transaction)
SETTLEDPosted to ledger, included in settlementFinal — included in bank's booksYes (but complex — needs adjustment)
DECLINEDAuthorization deniedNoneN/A
REVERSEDCompleted transaction was reversedReversed — funds returnedNo (creates new reversal)
VOIDEDTransaction cancelled before completionHold releasedN/A

ISO 8583 Message Structure

Most ATM transactions communicate with payment networks using ISO 8583 — a binary message format that has been the backbone of financial messaging since 1987. Each message consists of a header, a bitmap indicating which fields are present, and up to 128 data fields.

ISO 8583 Authorization Request (0100/0200)

  • MTI (Message Type Indicator): 0100 = Authorization Request, 0200 = Financial Transaction Request
  • Field 2 (PAN): Primary Account Number (encrypted)
  • Field 3 (Processing Code): 00 = Cash Withdrawal, 31 = Balance Inquiry, 50 = Transfer
  • Field 4 (Amount): Transaction amount in minor currency units
  • Field 7 (Transmission Date/Time): MMDDhhmmss
  • Field 11 (STAN): System Trace Audit Number (sequential per ATM)
  • Field 12/13 (Local/UTC Time): Transaction timestamp
  • Field 37 (Retrieval Reference): Unique reference for the transaction
  • Field 38 (Auth Code): Authorization code from issuer
  • Field 39 (Response Code): 00 = Approved, 51 = Insufficient Funds, 55 = Incorrect PIN
  • Field 55 (EMV Data): Chip card data including ARQC, AID, TVR, TSI

13. Cash Management & Replenishment

Cash management is an optimization problem that directly impacts bank profitability. Holding too much cash in ATMs earns zero interest; holding too little causes service outages and customer dissatisfaction. The goal is to maintain just enough cash to meet predicted demand while minimizing idle cash float.

graph TB subgraph Data_Sources["Data Sources"] HIST["Historical
Transaction Data"] CAL["Calendar Events
Holidays, Paydays"] LOC["Location Data
Mall vs Branch vs ATM"] WEATHER["Weather / Events
External Factors"] end subgraph Forecasting["ML Forecasting Engine"] MODEL["Demand Prediction
Model (LSTM/Prophet)"] OPT["Route Optimization
TSP Solver"] end subgraph Execution["Execution"] SCHEDULE["Replenishment
Schedule"] CIT["CIT Crew
Dispatch"] VERIFY["Cash Count
Verification"] end HIST & CAL & LOC & WEATHER --> MODEL MODEL -->|"Predicted daily
demand per ATM"| OPT OPT -->|"Optimal routes
and quantities"| SCHEDULE SCHEDULE --> CIT CIT --> VERIFY VERIFY -->|"Actual vs Predicted
feedback loop"| MODEL

Replenishment Strategy

StrategyDescriptionBest ForCost
Fixed ScheduleReplenish every N days regardless of levelLow-volume ATMs in stable areasLow (predictable CIT costs)
Threshold-BasedReplenish when cassette falls below thresholdMedium-volume ATMsMedium (responsive but unpredictable)
Demand-PredictedML model predicts when replenishment neededHigh-volume, variable-demand ATMsOptimized (lowest total cost)
HybridPredicted with threshold safety netMost real-world deploymentsBalanced

Route optimization for CIT crews is a variant of the Traveling Salesman Problem (TSP). Given a fleet of CIT vehicles and a set of ATMs to replenish, the system must minimize total travel time while respecting vehicle capacity, time windows, and security requirements (two-person crew for high-value stops). Modern systems use Google OR-Tools or custom heuristics to solve this NP-hard problem in near-optimal time.

14. Network Communication

ATM network communication must be both secure and resilient. ATMs connect to bank backends via encrypted VPN tunnels over dedicated lines, broadband internet, or 4G/LTE cellular connections. The communication protocol is typically ISO 8583 over TCP/IP, with TLS encryption as an additional layer.

graph LR subgraph ATM_Side["ATM Side"] APP["ATM Application"] ISO_LIB["ISO 8583
Library"] TLS_L["TLS 1.3
Layer"] TCP_L["TCP/IP"] NET["Physical
Network"] end subgraph Bank_Side["Bank Side"] NET2["Physical
Network"] TLS_B["TLS 1.3
Layer"] TCP_B["TCP/IP"] ISO_P["ISO 8583
Parser"] ROUTER["Message
Router"] end APP --> ISO_LIB --> TLS_L --> TCP_L --> NET NET --> NET2 --> TLS_B --> TCP_B --> ISO_P --> ROUTER

Store-and-Forward Mechanism

When network connectivity is lost, the ATM must continue operating in degraded mode. The store-and-forward mechanism allows the ATM to queue transactions locally and forward them when connectivity is restored. This requires careful handling of authorization — transactions that cannot be authorized in real-time are queued with a special status and processed later, subject to pre-approved offline limits.

public class StoreAndForwardManager
{
    private readonly ILocalTransactionStore _localStore;
    private readonly INetworkClient _networkClient;
    private readonly OfflinePolicy _offlinePolicy;

    public async Task<TransactionResult> ProcessTransactionAsync(
        TransactionRequest request)
    {
        if (await _networkClient.IsConnectedAsync())
        {
            // Online path: normal authorization
            return await ProcessOnlineAsync(request);
        }

        // Offline path: store and forward
        if (!_offlinePolicy.IsPermittedOffline(request))
        {
            return TransactionResult.Declined("Offline transactions not permitted");
        }

        if (!await _offlinePolicy.CheckOfflineLimitAsync(
            request.AtmId, request.Amount))
        {
            return TransactionResult.Declined("Offline limit exceeded");
        }

        // Store transaction locally for later forwarding
        var localTxn = new LocalTransaction
        {
            Id = Guid.NewGuid().ToString(),
            AtmId = request.AtmId,
            Amount = request.Amount,
            Type = request.Type,
            Pan = request.Pan,
            CreatedAt = DateTime.UtcNow,
            Status = LocalTxnStatus.Queued,
            RetryCount = 0
        };

        await _localStore.SaveAsync(localTxn);
        return TransactionResult.Queued(localTxn.Id);
    }

    public async Task ProcessForwardQueueAsync()
    {
        var queued = await _localStore.GetPendingAsync();

        foreach (var txn in queued)
        {
            try
            {
                var result = await ProcessOnlineAsync(
                    txn.ToTransactionRequest());

                if (result.IsSuccess)
                {
                    txn.Status = LocalTxnStatus.Forwarded;
                    txn.ForwardedAt = DateTime.UtcNow;
                }
                else if (result.IsDeclined)
                {
                    txn.Status = LocalTxnStatus.Declined;
                }
                // If network error, leave as Queued for retry
            }
            catch (NetworkException)
            {
                txn.RetryCount++;
                if (txn.RetryCount >= 10)
                    txn.Status = LocalTxnStatus.Failed;
            }

            await _localStore.UpdateAsync(txn);
        }
    }
}

15. Error Handling & Recovery

Error handling in ATM systems is paramount because errors involve real money. The system must handle three categories of errors: transaction errors (declined, timeout), hardware errors (dispenser jam, card reader failure), and system errors (database unavailable, network partition).

Reversal vs Void vs Adjustment

MechanismWhen UsedAccount ImpactComplexity
VoidTransaction authorized but not yet completedRelease hold, no debitLow
ReversalTransaction completed but needs undoingReverse debit, credit backMedium
AdjustmentManual correction by bank operationsManual debit/creditHigh (requires approval)

Dispense Mismatch Handling

Critical Scenario: A dispense mismatch occurs when the ATM dispenses a different amount than authorized. For example, the system authorized $200 but the dispenser sent 5 notes of $50 ($250) due to a multi-pick error. This must be detected and corrected immediately.

The dispense mismatch detection works as follows: the cash dispenser has optical pick sensors that count each note as it passes through the transport mechanism. The terminal software compares the actual count with the authorized count. If there is a discrepancy, the extra notes are retracted into the reject bin, and the transaction is adjusted to match the actual dispensed amount. If the customer has already taken the cash, the discrepancy is logged as a financial loss event and escalated for manual reconciliation.

Transaction journaling ensures that every state change is recorded before execution. The journal follows the write-ahead logging (WAL) pattern — the intended state change is written to persistent storage before the actual operation is performed. If the system crashes mid-transaction, recovery replays the journal to determine the final state.

16. Anti-Skimming & Security

ATM security is a multi-layered discipline encompassing physical, network, and application security. The most visible threat is card skimming — the installation of rogue devices that capture card data and PINs. Modern ATMs employ multiple countermeasures.

Anti-Skimming Technologies

TechnologyHow It WorksProtection Against
Jitter Card ReaderRandomizes card insertion speed to corrupt skimmer dataMagnetic stripe skimmers
EMV-Only ModeDisables magnetic stripe reading entirelyAll stripe-based skimming
Card Shimmer DetectionMonitors ICC communication for anomalies; shim is a thin device in the chip slotChip data interception (shimmers)
Camera DetectionIR sensors detect unauthorized cameras near PIN padPIN shoulder surfing / cameras
Tamper-Evident BezelsOverlay detects physical removal attemptsPhysical overlay attacks
Geolocation ValidationVerifies ATM is at its registered locationATM relocation attacks
Vibration SensorsDetect drilling or physical tamperingCash access attacks

Network security follows PCI DSS requirements strictly. All communication between ATM and backend is encrypted using TLS 1.3 with mutual authentication (mTLS). Card data is encrypted at the reader level using DUKPT (Derived Unique Key Per Transaction) — every transaction uses a unique encryption key derived from the card reader's base key and the transaction counter. This ensures that even if one transaction's key is compromised, all other transactions remain secure.

17. Multi-Bank & Interoperability

In most countries, ATMs are shared across banks through network switches. A customer with a Bank A card can withdraw cash from a Bank B ATM. This interoperability is managed through interchange networks that route transactions, handle settlement, and manage interchange fees.

graph TB subgraph Cardholders["Cardholders"] C1["Bank A
Customer"] C2["Bank B
Customer"] C3["Bank C
Customer"] end subgraph ATMs["ATM Network"] ATM_A["Bank A ATMs
(5,000)"] ATM_B["Bank B ATMs
(8,000)"] ATM_C["Bank C ATMs
(3,000)"] end subgraph Switch["Network Switch"] NS["Interbank Switch
e.g. Visa PLUS,
Mastercard Cirrus,
NPCI (India)"] end subgraph Settlement["Settlement"] CLS["Clearing &
Settlement"] end C1 --> ATM_B C2 --> ATM_C C3 --> ATM_A ATM_A --> NS ATM_B --> NS ATM_C --> NS NS --> CLS CLS -->|"Interchange
Fee Settlement"| C1 & C2 & C3

Transaction Routing Decision

When a card is presented at a shared ATM, the switch determines the routing path based on: (1) card network affinity (Visa cards prefer Visa rails), (2) interchange fee optimization, (3) network availability, and (4) issuer connectivity. The routing decision happens in milliseconds and must account for real-time network health metrics.

NetworkTypeCoverageInterchange Fee (typical)
Visa PLUSGlobal200+ countries$0.50 - $2.50 per transaction
Mastercard CirrusGlobal200+ countries$0.50 - $2.50 per transaction
NPCI (RuPay)National (India)India₹0.30 - ₹1.00 per transaction
LINK (UK)National (UK)United Kingdom£0.20 - £0.30 per transaction
STAR (US)National (US)United States$0.15 - $0.50 per transaction

18. Deposit Processing

Modern ATMs support envelope-free deposits using image capture and note validation technology. When a customer deposits cash or checks, the ATM's deposit module scans each item, captures images, validates denominations (for cash) or reads MICR data (for checks), and provides a real-time count to the customer for confirmation.

Cash Deposit Flow

  1. Customer selects deposit and places a stack of notes in the acceptor slot
  2. The deposit module feeds notes one at a time through the validation path
  3. Each note is scanned for denomination (UV, magnetic, and optical sensors), authenticity (counterfeit detection), and fitness (damaged notes rejected)
  4. The validated count and total are displayed to the customer for confirmation
  5. Customer confirms — notes are moved from the staging area to the deposit cassette
  6. Account is credited immediately (provisional) or after backend processing (deferred)
  7. Images of deposited items are stored for reconciliation and dispute resolution

Check Deposit Processing

Check deposits use high-resolution image scanners to capture the front and back of each check. The MICR (Magnetic Ink Character Recognition) line is read to extract the routing number, account number, and check number. Image quality is validated against ACI (Accuity Check Image standards). The check images are transmitted to the bank's back-office for automated clearing house (ACH) processing or image exchange.

19. Database Design

The database design for an ATM system is optimized for high write throughput (every transaction is a write), strong consistency for financial data, and audit trail preservation. The system uses a primary OLTP database (PostgreSQL or Oracle) with time-series analytics for monitoring.

Transaction Journal Schema

-- Transaction Journal: Append-only table for all ATM transactions
CREATE TABLE transaction_journal (
    journal_id        BIGSERIAL PRIMARY KEY,
    transaction_id    UUID NOT NULL UNIQUE,
    session_id        UUID NOT NULL,
    atm_id            VARCHAR(10) NOT NULL,
    card_token        VARCHAR(64) NOT NULL,
    account_id        VARCHAR(20) NOT NULL,
    transaction_type  VARCHAR(20) NOT NULL,
    amount            DECIMAL(15,2) NOT NULL,
    currency_code     CHAR(3) NOT NULL DEFAULT 'USD',
    status            VARCHAR(20) NOT NULL,
    auth_code         VARCHAR(20),
    reversal_of       UUID REFERENCES transaction_journal(transaction_id),
    network_trace     VARCHAR(20),
    iso_message       BYTEA,
    emv_data          JSONB,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    settled_at        TIMESTAMPTZ,
    metadata          JSONB
);

-- Index for real-time ATM monitoring
CREATE INDEX idx_journal_atm_status ON transaction_journal(atm_id, status, created_at DESC);
-- Index for settlement processing
CREATE INDEX idx_journal_settlement ON transaction_journal(status, created_at) WHERE status = 'COMPLETED';
-- Index for account lookup
CREATE INDEX idx_journal_account ON transaction_journal(account_id, created_at DESC);

-- ATM State Table
CREATE TABLE atm_state (
    atm_id            VARCHAR(10) PRIMARY KEY,
    status            VARCHAR(20) NOT NULL,
    current_session_id UUID,
    latitude          DECIMAL(10,7),
    longitude         DECIMAL(10,7),
    software_version  VARCHAR(20),
    last_heartbeat    TIMESTAMPTZ,
    last_replenished  TIMESTAMPTZ,
    total_cash        DECIMAL(15,2),
    daily_txn_count   INT DEFAULT 0,
    daily_cash_dispensed DECIMAL(15,2) DEFAULT 0,
    config            JSONB,
    updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Cash Cassette Levels
CREATE TABLE cash_cassettes (
    cassette_id       SERIAL PRIMARY KEY,
    atm_id            VARCHAR(10) NOT NULL REFERENCES atm_state(atm_id),
    position          INT NOT NULL,
    denomination      INT NOT NULL,
    capacity          INT NOT NULL,
    current_count     INT NOT NULL,
    low_threshold     INT NOT NULL,
    status            VARCHAR(20) NOT NULL,
    last_replenished  TIMESTAMPTZ,
    UNIQUE(atm_id, position)
);

-- Audit Trail: Immutable append-only log
CREATE TABLE audit_trail (
    event_id          BIGSERIAL PRIMARY KEY,
    atm_id            VARCHAR(10) NOT NULL,
    session_id        UUID,
    event_type        VARCHAR(50) NOT NULL,
    event_detail      TEXT,
    severity          VARCHAR(10) NOT NULL,
    source_ip         INET,
    event_time        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Partition audit trail by month for performance
CREATE TABLE audit_trail_2026_07 PARTITION OF audit_trail
    FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');

20. Caching Strategy

Caching in the ATM system serves two purposes: reducing backend load during peak hours and enabling offline/degraded operation. The cache must be carefully managed to avoid stale financial data.

Cache KeyDataTTLInvalidationStale Policy
balance:{account_id}Available balance30 secondsOn every transactionNever serve stale balance
config:{atm_id}ATM configuration, limits, denominations5 minutesOn config pushServe stale if backend unreachable
card:{card_token_hash}Card status, linked accounts60 secondsOn status changeNever serve stale card status
limits:{account_id}Daily withdrawal/deposit limits60 secondsOn transactionConservative: assume limit reached
fraud_score:{card_token}Current fraud risk score5 minutesOn new score calculationServe stale, flag for review
exchange_rate:{currency}Forex rates for multi-currency15 minutesScheduled refreshServe stale with rate advisory
Critical Rule: Balance cache must never be used for authorization decisions in a way that could overdraw an account. The cache provides a fast pre-check, but the final authorization always verifies against the authoritative database. The cache is a read-through cache with write-through invalidation on every balance-modifying transaction.

21. Multi-Region Design

Banks with global ATM fleets must handle multi-region operations with different currencies, regulations, and network providers. The multi-region design uses a hub-and-spoke model with regional processing centers.

graph TB subgraph Global_Hub["Global Hub (Primary DC)"] GLOBAL_SWITCH["Global Transaction
Router"] GLOBAL_DB["Master Database
Replicated"] GLOBAL_FRAUD["Global Fraud
Detection"] end subgraph Region_NA["North America Region"] NA_SWITCH["NA Transaction
Processor"] NA_DB["NA Database
Replica"] NA_HSM["NA HSM Cluster"] NA_ATMS["15,000 ATMs"] end subgraph Region_EU["Europe Region"] EU_SWITCH["EU Transaction
Processor"] EU_DB["EU Database
Replica"] EU_HSM["EU HSM Cluster"] EU_ATMS["20,000 ATMs"] end subgraph Region_APAC["Asia-Pacific Region"] APAC_SWITCH["APAC Transaction
Processor"] APAC_DB["APAC Database
Replica"] APAC_HSM["APAC HSM Cluster"] APAC_ATMS["25,000 ATMs"] end NA_ATMS --> NA_SWITCH EU_ATMS --> EU_SWITCH APAC_ATMS --> APAC_SWITCH NA_SWITCH --> GLOBAL_SWITCH EU_SWITCH --> GLOBAL_SWITCH APAC_SWITCH --> GLOBAL_SWITCH GLOBAL_SWITCH --> GLOBAL_DB GLOBAL_SWITCH --> GLOBAL_FRAUD GLOBAL_DB -.->|"Async Replication"| NA_DB & EU_DB & APAC_DB

Regional transaction processing ensures that domestic transactions stay within the region for low latency and regulatory compliance (data residency laws). Cross-region transactions (e.g., a European card used at an Asian ATM) are routed through the global hub for authorization with the issuing bank's region. The global fraud detection engine aggregates signals across all regions to identify patterns like card cloning across multiple countries.

22. Cost Estimation

Cost ComponentPer Unit (Annual)Fleet (50K ATMs)Notes
ATM Hardware (amortized)$3,000$150M5-year lifecycle for $15K machine
ATM Site Lease$6,000 - $24,000$600MVaries dramatically by location
Cash Float$40,000 per ATM$2B (not a cost — but opportunity cost of capital)At 5% interest rate = $100M/year opportunity cost
CIT (Cash-In-Transit)$5,000$250MIncludes armored vehicles, personnel, insurance
Network Connectivity$1,200$60MVPN, dual connections for redundancy
Software Licenses$2,000$100MSwitch software, monitoring, security
Maintenance & Repairs$3,000$150MParts, technician dispatches
PCI DSS Compliance$500$25MAudits, penetration testing, certifications
Interchange Fees$0.50 - $2.50 per txn$75MPaid on interbank transactions
Cloud Infrastructure$500$25MBackend servers, databases, monitoring
Total Annual Operating Cost$15K - $40K per ATM$935M - $985MExcluding cash float

The total cost of ownership reveals why ATM placement is a strategic business decision. A well-placed ATM in a high-traffic location might process 500+ transactions per day, generating significant interchange revenue. A poorly placed ATM in a low-traffic area might barely cover its operating costs. The break-even point is typically around 70-100 transactions per day for a full-service ATM.

23. Interview Q&A

Q1: What happens when the ATM dispenses more cash than authorized (dispense mismatch)?

The ATM's optical pick sensors count each note individually as it passes through the transport. If the actual count exceeds the authorized count, the extra notes are immediately retracted into a secure reject bin. The customer receives only the authorized amount. If the customer has already taken the cash before the discrepancy is detected (a very rare race condition), the system logs a financial loss event, takes a photograph from the PIN camera, and escalates to the bank's operations team for investigation. The transaction record includes the exact dispense amount and sensor readings for reconciliation.

Q2: How does EMV prevent card cloning at ATMs?

EMV uses dynamic authentication — each transaction generates a unique cryptogram (ARQC) using a secret key embedded in the chip and known only to the issuing bank. Unlike magnetic stripe cards where the data is static (and therefore cloneable), chip card data changes with every transaction. Even if an attacker captures the complete EMV data from one transaction, they cannot use it for another transaction because the cryptogram will be different. The chip also supports mutual authentication — the card validates the terminal's certificate before generating the cryptogram, preventing rogue ATMs from harvesting card data.

Q3: How would you design the cash replenishment forecasting system?

The forecasting system uses a combination of historical transaction patterns, calendar effects (paydays, holidays, month-end), location-specific factors (mall traffic vs. residential area), and external signals (events, weather). The model is typically a time-series forecaster (Prophet, LSTM, or gradient boosting) trained on 6-12 months of daily transaction volumes per ATM. The output is a daily demand forecast with confidence intervals. Replenishment is scheduled when the predicted cash level falls below the threshold, with a safety buffer. Route optimization then groups nearby ATMs into efficient CIT routes using a TSP solver. The system feeds actual demand back to the model for continuous improvement.

Q4: How do you handle network partitions between ATM and bank backend?

The system uses a store-and-forward pattern. When the network is down, the ATM enters offline mode with restricted functionality — it allows small withdrawals (within pre-configured offline limits, typically $200/day) using locally cached card verification data. Transactions are queued in a persistent local store with WAL (write-ahead logging). When connectivity is restored, the queue is forwarded in order. The offline limits are conservative and configurable per ATM based on its location risk profile. PIN verification in offline mode uses offline PIN (EMV chip-based verification) where the card itself validates the PIN.

Q5: Explain the ISO 8583 message flow for a cash withdrawal.

The flow is: (1) ATM sends a 0100 (Authorization Request) with PAN, amount, processing code 00 (cash withdrawal), STAN, and EMV data. (2) The switch routes it to the issuing bank. (3) The issuer validates the card, checks balance, runs fraud checks, and generates an ARPC (Authorization Response Cryptogram). (4) The issuer responds with 0110 (Authorization Response) containing response code (00 = approved), auth code, and ARPC. (5) If approved, ATM dispenses cash. (6) ATM sends 0200 (Financial Transaction Request) to confirm the dispense. (7) Issuer responds with 0210 confirming settlement. The ARQC/ARPC mechanism ensures end-to-end cryptographic integrity.

Q6: How do you prevent double-spending if the ATM crashes after dispensing but before recording the transaction?

This is handled through the write-ahead logging (WAL) pattern and transaction state machine. The ATM records DISPENSING state before sending the dispense command to hardware. If the ATM crashes after dispensing but before confirming completion, on recovery it reads the journal, finds the DISPENSING state, and attempts to determine if the customer took the cash (via the take sensor log). If the cash was taken, it forwards the completion to the backend. If not, it retracts any remaining cash and reverses the authorization. The backend uses the unique transaction ID for idempotency — retrying a completion with the same ID returns the original result without duplicate posting.

Q7: How would you design the multi-currency withdrawal feature?

Multi-currency dynamic currency conversion (DCC) requires real-time exchange rate feeds from a rate provider (e.g., Refinitiv, Bloomberg). The flow: (1) Card is inserted, BIN identifies the card's domestic currency. (2) ATM offers choice: withdraw in local currency or card's home currency. (3) If DCC selected, the exchange rate is fetched (cached for 15 minutes), markup is applied per regulatory limits, and the converted amount is displayed. (4) Customer confirms. (5) Transaction is processed in local currency with the DCC rate embedded in the ISO 58 message field 10 (currency code of settlement). The rate provider, markup percentage, and DCC consent must all be logged for regulatory compliance.

Q8: What are the challenges with ATM session management in a distributed system?

Key challenges: (1) Session affinity — a session must be pinned to a specific ATM and backend instance for the duration; network switches may route different messages to different backend instances. (2) Timeout management — both ATM-side and server-side timeouts must be coordinated; a server-side timeout must not cause the ATM to enter an inconsistent state. (3) Concurrent sessions — a single ATM can only have one active customer session, but the backend must handle 50,000 concurrent sessions across the fleet. (4) State synchronization — if the backend processes a state transition that the ATM hasn't received (due to network delay), the system must handle the out-of-order message gracefully. Solution: each session has a monotonically increasing sequence number, and messages with stale sequence numbers are rejected.

Q9: How does the HSM (Hardware Security Module) architecture work for ATM PIN verification?

The HSM is a tamper-resistant hardware device that performs cryptographic operations in a secure enclave. The architecture: (1) Key Hierarchy — Master Key (stored in HSM, never exported), Zone PIN Key (ZPK, encrypted under Master Key, unique per ATM zone), PIN Encryption Key (PEK, used by the EPP to encrypt the PIN). (2) Verification Flow — The encrypted PIN block arrives at the HSM, which decrypts it using the ZPK, extracts the plaintext PIN, and compares it against the PIN reference from the card management system. (3) Security Properties — the HSM's internal key storage is protected by tamper-responsive hardware; if someone tries to physically breach the casing, all keys are zeroized. The HSM also provides key ceremony procedures for loading master keys using split knowledge (multiple people each enter a portion of the key). (4) Cluster operation — HSMs run in pairs (primary + secondary) with real-time key synchronization. If the primary fails, the secondary takes over without service interruption.

Q10: How would you monitor and alert on ATM fleet health in real-time?

The monitoring system has four tiers: (1) Heartbeat monitoring — each ATM sends a heartbeat every 60 seconds. Missing 3 consecutive heartbeats triggers a "communication lost" alert. (2) Transaction health — real-time dashboards track success rate, average response time, and error codes per ATM and across the fleet. A sudden drop in success rate at a specific ATM triggers an alert (could indicate hardware failure or network issue). (3) Cash level monitoring — cassette levels are updated after every dispense/deposit and on each heartbeat. When any cassette falls below its threshold, a replenishment alert is generated and routed to the CIT scheduling system. (4) Security alerts — tamper detection, anti-skimming sensors, and unusual transaction patterns (e.g., rapid-fire small withdrawals indicating card testing) trigger immediate security alerts with severity levels. All alerts flow into a centralized NOC (Network Operations Center) dashboard with automated escalation policies.

Q11: Design the cardless withdrawal feature (QR code or one-time code).

Cardless withdrawal eliminates the physical card entirely. Flow: (1) Customer opens their bank's mobile app and requests a cardless withdrawal. (2) The app generates a one-time code (6-digit numeric) or QR code, valid for 15-30 minutes, tied to the customer's account and the requested amount. (3) Customer goes to any ATM and selects "Cardless Withdrawal." (4) Enters the one-time code (or scans QR code). (5) ATM sends the code to the backend for validation. (6) Backend verifies: code matches, not expired, amount is within limits, account is in good standing. (7) If valid, dispenses cash and marks the code as used. Security considerations: one-time codes are rate-limited (3 attempts), time-limited, single-use, and optionally geo-fenced (must be used at an ATM within X km of the customer's phone GPS).

Q12: How do you handle reconciliation when the ATM's cash count doesn't match the system's expected count?

Reconciliation runs daily (end-of-day) and involves comparing three sources of truth: (1) the system's transaction journal (expected cash movement), (2) the ATM's hardware-reported cassette levels (actual cash), and (3) the CIT replenishment records (cash added/removed). Any discrepancy triggers an investigation workflow. Common causes: multi-pick errors (dispensed extra notes), retract events (customer didn't take cash), sensor miscounts, or CIT counting errors. The reconciliation engine calculates the expected level by starting with the last known good count, adding CIT deposits, subtracting completed withdrawals, and comparing with the current hardware count. Differences are categorized as: minor (within sensor tolerance, auto-adjusted), significant (requires manual review), or critical (potential fraud, escalated immediately).

24. Full C# Implementation

Below is a comprehensive C# implementation of the core ATM system classes including the state machine, cash dispenser, card processor, and transaction manager. This implementation covers approximately 350 lines of production-quality code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ATMSystem.Core
{
    // =====================================================
    // ENUMS
    // =====================================================

    public enum SessionState
    {
        IDLE,
        CARD_INSERTED,
        PIN_ENTRY,
        PIN_VERIFIED,
        MENU_DISPLAYED,
        AMOUNT_ENTRY,
        AUTHORIZING,
        DISPENSING,
        CASH_PRESENTED,
        DEPOSIT_ACCEPTING,
        DECLINED,
        COMPLETE,
        CARD_EJECTING,
        BLOCKED
    }

    public enum TransactionType
    {
        WITHDRAWAL,
        DEPOSIT,
        BALANCE_INQUIRY,
        TRANSFER,
        PIN_CHANGE,
        MINI_STATEMENT
    }

    public enum TransactionStatus
    {
        PENDING,
        AUTHORIZED,
        COMPLETED,
        DECLINED,
        REVERSED,
        VOIDED,
        SETTLED
    }

    public enum CassetteType
    {
        DISPENSE,
        DEPOSIT,
        REJECT
    }

    // =====================================================
    // MODELS
    // =====================================================

    public class AtmSession
    {
        public string SessionId { get; set; } = Guid.NewGuid().ToString("N");
        public string AtmId { get; set; }
        public SessionState State { get; set; } = SessionState.IDLE;
        public string CardToken { get; set; }
        public string Pan { get; set; }
        public int PinAttemptCount { get; set; }
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public DateTime LastActivity { get; set; } = DateTime.UtcNow;
        public TransactionType? SelectedTransaction { get; set; }
        public decimal? RequestedAmount { get; set; }
        public string AccountId { get; set; }

        public bool IsTimedOut(TimeSpan timeout) =>
            DateTime.UtcNow - LastActivity > timeout;

        public void Touch() => LastActivity = DateTime.UtcNow;
    }

    public class AtmTransaction
    {
        public string TransactionId { get; set; } = Guid.NewGuid().ToString("N");
        public string SessionId { get; set; }
        public string AtmId { get; set; }
        public string AccountId { get; set; }
        public TransactionType Type { get; set; }
        public decimal Amount { get; set; }
        public TransactionStatus Status { get; set; } = TransactionStatus.PENDING;
        public string AuthCode { get; set; }
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public DateTime? CompletedAt { get; set; }
        public List<DenominationResult> DispensedNotes { get; set; } = new();
    }

    public class Cassette
    {
        public int Id { get; set; }
        public int Denomination { get; set; }
        public int Capacity { get; set; }
        public int CurrentCount { get; set; }
        public CassetteType Type { get; set; }
        public int LowThreshold { get; set; }

        public bool HasSufficient(int requested) => CurrentCount >= requested;
        public bool IsLow => CurrentCount <= LowThreshold;

        public void Dispense(int count)
        {
            if (count > CurrentCount)
                throw new InvalidOperationException(
                    $"Cannot dispense {count} notes from cassette with {CurrentCount}");
            CurrentCount -= count;
        }
    }

    public class DenominationResult
    {
        public int Denomination { get; set; }
        public int Count { get; set; }
        public int Total => Denomination * Count;
    }

    public class DispensePlan
    {
        public List<DenominationResult> Notes { get; set; } = new();
        public int TotalNotes => Notes.Sum(n => n.Count);
        public int TotalAmount => Notes.Sum(n => n.Total);
        public bool IsSuccess => Notes.Any();
    }

    public class AuthorizationResult
    {
        public bool IsApproved { get; set; }
        public string AuthCode { get; set; }
        public string DeclineReason { get; set; }
        public decimal AvailableBalance { get; set; }

        public static AuthorizationResult Approved(string authCode, decimal balance) =>
            new() { IsApproved = true, AuthCode = authCode, AvailableBalance = balance };

        public static AuthorizationResult Declined(string reason) =>
            new() { IsApproved = false, DeclineReason = reason };
    }

    // =====================================================
    // CASH DISPENSER
    // =====================================================

    public class CashDispenser
    {
        private readonly List<Cassette> _cassettes;
        private const int MAX_NOTES_PER_DISPENSE = 40;

        public CashDispenser(List<Cassette> cassettes)
        {
            _cassettes = cassettes.Where(c => c.Type == CassetteType.DISPENSE)
                                   .OrderByDescending(c => c.Denomination)
                                   .ToList();
        }

        public DispensePlan CalculateDispense(decimal amount)
        {
            int targetAmount = (int)amount;
            var plan = new DispensePlan();
            int remaining = targetAmount;

            foreach (var cassette in _cassettes)
            {
                if (remaining <= 0) break;
                if (!cassette.HasSufficient(1)) continue;

                int notesNeeded = remaining / cassette.Denomination;
                int notesToDispense = Math.Min(notesNeeded, cassette.CurrentCount);
                notesToDispense = Math.Min(notesToDispense,
                    MAX_NOTES_PER_DISPENSE - plan.TotalNotes);

                if (notesToDispense > 0)
                {
                    plan.Notes.Add(new DenominationResult
                    {
                        Denomination = cassette.Denomination,
                        Count = notesToDispense
                    });
                    remaining -= cassette.Denomination * notesToDispense;
                }
            }

            if (remaining > 0 && plan.IsSuccess)
            {
                // Cannot make exact amount; return partial or decline
                return CanFulfillExact(targetAmount) ? CalculateDispense(amount) : plan;
            }

            return plan;
        }

        public void ExecuteDispense(DispensePlan plan)
        {
            foreach (var note in plan.Notes)
            {
                var cassette = _cassettes.First(c => c.Denomination == note.Denomination);
                cassette.Dispense(note.Count);
            }
        }

        public void RevertDispense(DispensePlan plan)
        {
            foreach (var note in plan.Notes)
            {
                var cassette = _cassettes.First(c => c.Denomination == note.Denomination);
                cassette.CurrentCount += note.Count;
            }
        }

        public bool CanFulfillExact(int amount)
        {
            return CalculateDispense(amount).TotalAmount == amount;
        }

        public Dictionary<int, int> GetCassetteLevels() =>
            _cassettes.ToDictionary(c => c.Denomination, c => c.CurrentCount);

        public List<Cassette> GetLowCassettes() =>
            _cassettes.Where(c => c.IsLow).ToList();

        public decimal GetTotalCash() =>
            _cassettes.Sum(c => (decimal)(c.CurrentCount * c.Denomination));
    }

    // =====================================================
    // CARD PROCESSOR
    // =====================================================

    public class CardProcessor
    {
        private readonly IHsmService _hsmService;
        private const int MAX_PIN_ATTEMPTS = 3;

        public CardProcessor(IHsmService hsmService)
        {
            _hsmService = hsmService;
        }

        public async Task<bool> ValidateCardAsync(string cardTrack2Encrypted)
        {
            // Parse track 2 data (encrypted)
            // Validate Luhn check digit
            // Check card status with card management system
            await Task.Delay(50); // Simulate card validation
            return !string.IsNullOrEmpty(cardTrack2Encrypted);
        }

        public async Task<PinVerifyResult> VerifyPinAsync(
            string encryptedPinBlock,
            string pan,
            string zoneKeyIndex)
        {
            try
            {
                var result = await _hsmService.VerifyPinAsync(
                    encryptedPinBlock, pan, zoneKeyIndex);
                return result;
            }
            catch (HsmException)
            {
                return PinVerifyResult.SystemError;
            }
        }

        public async Task<string> GetCardTokenAsync(string pan)
        {
            // In production: lookup tokenized card reference
            await Task.Delay(10);
            return Convert.ToHexString(
                System.Security.Cryptography.SHA256.HashData(
                    System.Text.Encoding.UTF8.GetBytes(pan)))[..32];
        }

        public bool CanRetryPin(AtmSession session) =>
            session.PinAttemptCount < MAX_PIN_ATTEMPTS;
    }

    public enum PinVerifyResult
    {
        Valid,
        Invalid,
        Blocked,
        SystemError
    }

    public interface IHsmService
    {
        Task<PinVerifyResult> VerifyPinAsync(
            string encryptedPinBlock, string pan, string zoneKeyIndex);
        Task<string> GenerateKeyAsync(string keyType);
    }

    public class HsmException : Exception
    {
        public HsmException(string message) : base(message) { }
    }

    // =====================================================
    // TRANSACTION MANAGER
    // =====================================================

    public class TransactionManager
    {
        private readonly IAccountService _accountService;
        private readonly ITransactionStore _transactionStore;
        private readonly IAuthorizationService _authService;

        public TransactionManager(
            IAccountService accountService,
            ITransactionStore transactionStore,
            IAuthorizationService authService)
        {
            _accountService = accountService;
            _transactionStore = transactionStore;
            _authService = authService;
        }

        public async Task<AtmTransaction> AuthorizeWithdrawalAsync(
            string sessionId,
            string accountId,
            decimal amount,
            string atmId)
        {
            var transaction = new AtmTransaction
            {
                SessionId = sessionId,
                AccountId = accountId,
                AtmId = atmId,
                Type = TransactionType.WITHDRAWAL,
                Amount = amount,
                Status = TransactionStatus.PENDING
            };

            await _transactionStore.SaveAsync(transaction);

            // Check available balance
            var balance = await _accountService.GetAvailableBalanceAsync(accountId);
            if (balance < amount)
            {
                transaction.Status = TransactionStatus.DECLINED;
                await _transactionStore.UpdateAsync(transaction);
                throw new TransactionDeclinedException("Insufficient funds");
            }

            // Check daily limits
            var dailyTotal = await _transactionStore.GetDailyTotalAsync(
                accountId, TransactionType.WITHDRAWAL);
            if (dailyTotal + amount > 10000) // $10,000 daily limit
            {
                transaction.Status = TransactionStatus.DECLINED;
                await _transactionStore.UpdateAsync(transaction);
                throw new TransactionDeclinedException("Daily limit exceeded");
            }

            // Hold funds
            await _accountService.HoldFundsAsync(accountId, amount);

            // Authorize
            var authResult = await _authService.AuthorizeAsync(transaction);
            if (!authResult.IsApproved)
            {
                await _accountService.ReleaseHoldAsync(accountId, amount);
                transaction.Status = TransactionStatus.DECLINED;
                await _transactionStore.UpdateAsync(transaction);
                throw new TransactionDeclinedException(authResult.DeclineReason);
            }

            transaction.AuthCode = authResult.AuthCode;
            transaction.Status = TransactionStatus.AUTHORIZED;
            await _transactionStore.UpdateAsync(transaction);

            return transaction;
        }

        public async Task CompleteTransactionAsync(
            AtmTransaction transaction,
            List<DenominationResult> dispensedNotes)
        {
            transaction.DispensedNotes = dispensedNotes;
            transaction.Status = TransactionStatus.COMPLETED;
            transaction.CompletedAt = DateTime.UtcNow;
            await _transactionStore.UpdateAsync(transaction);

            // Convert hold to actual debit
            await _accountService.DebitAsync(
                transaction.AccountId, transaction.Amount);
        }

        public async Task VoidTransactionAsync(AtmTransaction transaction)
        {
            await _accountService.ReleaseHoldAsync(
                transaction.AccountId, transaction.Amount);

            transaction.Status = TransactionStatus.VOIDED;
            transaction.CompletedAt = DateTime.UtcNow;
            await _transactionStore.UpdateAsync(transaction);
        }

        public async Task<AtmTransaction> ReverseTransactionAsync(
            string transactionId)
        {
            var original = await _transactionStore.GetByIdAsync(transactionId);
            if (original == null)
                throw new InvalidOperationException("Transaction not found");

            if (original.Status != TransactionStatus.COMPLETED)
                throw new InvalidOperationException("Only completed transactions can be reversed");

            // Credit back
            await _accountService.CreditAsync(original.AccountId, original.Amount);

            original.Status = TransactionStatus.REVERSED;
            original.CompletedAt = DateTime.UtcNow;
            await _transactionStore.UpdateAsync(original);

            return original;
        }
    }

    public class TransactionDeclinedException : Exception
    {
        public TransactionDeclinedException(string reason) : base(reason) { }
    }

    // =====================================================
    // ATM SESSION CONTROLLER (STATE MACHINE)
    // =====================================================

    public class AtmSessionController
    {
        private readonly CashDispenser _dispenser;
        private readonly CardProcessor _cardProcessor;
        private readonly TransactionManager _txnManager;
        private AtmSession _session;

        public AtmSessionController(
            CashDispenser dispenser,
            CardProcessor cardProcessor,
            TransactionManager txnManager,
            string atmId)
        {
            _dispenser = dispenser;
            _cardProcessor = cardProcessor;
            _txnManager = txnManager;
            _session = new AtmSession { AtmId = atmId };
        }

        public AtmSession CurrentSession => _session;

        public async Task OnCardInsertedAsync(string cardTrack2Encrypted)
        {
            EnsureState(SessionState.IDLE, "Card insert");

            var isValid = await _cardProcessor.ValidateCardAsync(cardTrack2Encrypted);
            if (!isValid)
            {
                TransitionTo(SessionState.CARD_EJECTING);
                return;
            }

            _session.CardToken = await _cardProcessor.GetCardTokenAsync(cardTrack2Encrypted);
            _session.Pan = cardTrack2Encrypted; // In production: decrypted PAN
            TransitionTo(SessionState.CARD_INSERTED);
            TransitionTo(SessionState.PIN_ENTRY);
        }

        public async Task<bool> OnPinEnteredAsync(string encryptedPinBlock)
        {
            EnsureState(SessionState.PIN_ENTRY, "PIN entry");

            if (!_cardProcessor.CanRetryPin(_session))
            {
                TransitionTo(SessionState.BLOCKED);
                return false;
            }

            var result = await _cardProcessor.VerifyPinAsync(
                encryptedPinBlock, _session.Pan, "zone-01");
            _session.PinAttemptCount++;

            if (result == PinVerifyResult.Valid)
            {
                TransitionTo(SessionState.PIN_VERIFIED);
                TransitionTo(SessionState.MENU_DISPLAYED);
                return true;
            }

            if (result == PinVerifyResult.Blocked || !_cardProcessor.CanRetryPin(_session))
            {
                TransitionTo(SessionState.BLOCKED);
            }

            return false;
        }

        public void SelectTransaction(TransactionType type)
        {
            EnsureState(SessionState.MENU_DISPLAYED, "Select transaction");
            _session.SelectedTransaction = type;

            if (type == TransactionType.BALANCE_INQUIRY)
            {
                TransitionTo(SessionState.AMOUNT_ENTRY);
            }
            else
            {
                TransitionTo(SessionState.AMOUNT_ENTRY);
            }
        }

        public async Task<AtmTransaction> EnterAmountAsync(decimal amount)
        {
            EnsureState(SessionState.AMOUNT_ENTRY, "Enter amount");
            _session.RequestedAmount = amount;
            TransitionTo(SessionState.AUTHORIZING);

            try
            {
                var transaction = await _txnManager.AuthorizeWithdrawalAsync(
                    _session.SessionId,
                    _session.AccountId,
                    amount,
                    _session.AtmId);

                TransitionTo(SessionState.DISPENSING);
                return transaction;
            }
            catch (TransactionDeclinedException ex)
            {
                TransitionTo(SessionState.DECLINED);
                throw;
            }
        }

        public async Task<DispensePlan> DispenseCashAsync(AtmTransaction transaction)
        {
            EnsureState(SessionState.DISPENSING, "Dispense cash");

            var plan = _dispenser.CalculateDispense(transaction.Amount);
            if (!plan.IsSuccess)
            {
                await _txnManager.VoidTransactionAsync(transaction);
                TransitionTo(SessionState.DECLINED);
                throw new InsufficientCashException("Cannot fulfill exact amount");
            }

            try
            {
                _dispenser.ExecuteDispense(plan);
                TransitionTo(SessionState.CASH_PRESENTED);
                return plan;
            }
            catch (Exception ex)
            {
                _dispenser.RevertDispense(plan);
                await _txnManager.VoidTransactionAsync(transaction);
                TransitionTo(SessionState.DECLINED);
                throw new DispenseHardwareException("Dispenser error", ex);
            }
        }

        public async Task CompleteAsync(AtmTransaction transaction)
        {
            EnsureState(SessionState.CASH_PRESENTED, "Complete");

            await _txnManager.CompleteTransactionAsync(
                transaction, transaction.DispensedNotes);

            TransitionTo(SessionState.COMPLETE);
            TransitionTo(SessionState.CARD_EJECTING);
        }

        public void EndSession()
        {
            _session = new AtmSession { AtmId = _session.AtmId };
        }

        private void TransitionTo(SessionState newState)
        {
            var oldState = _session.State;
            ValidateTransition(oldState, newState);
            _session.State = newState;
            _session.Touch();
        }

        private void EnsureState(SessionState expected, string operation)
        {
            if (_session.State != expected)
                throw new InvalidStateException(
                    $"Cannot perform '{operation}' in state {_session.State}. " +
                    $"Expected: {expected}");
        }

        private static readonly Dictionary<SessionState, HashSet<SessionState>>
            ValidTransitions = new()
        {
            [SessionState.IDLE] = new() { SessionState.CARD_INSERTED },
            [SessionState.CARD_INSERTED] = new() { SessionState.PIN_ENTRY, SessionState.CARD_EJECTING },
            [SessionState.PIN_ENTRY] = new() { SessionState.PIN_VERIFIED, SessionState.BLOCKED, SessionState.CARD_EJECTING },
            [SessionState.PIN_VERIFIED] = new() { SessionState.MENU_DISPLAYED },
            [SessionState.MENU_DISPLAYED] = new() { SessionState.AMOUNT_ENTRY, SessionState.DEPOSIT_ACCEPTING },
            [SessionState.AMOUNT_ENTRY] = new() { SessionState.AUTHORIZING, SessionState.MENU_DISPLAYED },
            [SessionState.AUTHORIZING] = new() { SessionState.DISPENSING, SessionState.DECLINED, SessionState.MENU_DISPLAYED },
            [SessionState.DISPENSING] = new() { SessionState.CASH_PRESENTED, SessionState.DECLINED },
            [SessionState.CASH_PRESENTED] = new() { SessionState.COMPLETE, SessionState.CARD_EJECTING },
            [SessionState.DECLINED] = new() { SessionState.MENU_DISPLAYED, SessionState.CARD_EJECTING },
            [SessionState.COMPLETE] = new() { SessionState.CARD_EJECTING },
            [SessionState.CARD_EJECTING] = new() { SessionState.IDLE },
            [SessionState.BLOCKED] = new() { SessionState.CARD_EJECTING },
            [SessionState.DEPOSIT_ACCEPTING] = new() { SessionState.MENU_DISPLAYED, SessionState.DECLINED }
        };

        private static void ValidateTransition(SessionState from, SessionState to)
        {
            if (!ValidTransitions.ContainsKey(from) ||
                !ValidTransitions[from].Contains(to))
            {
                throw new InvalidStateException(
                    $"Invalid state transition: {from} → {to}");
            }
        }
    }

    public class InvalidStateException : Exception
    {
        public InvalidStateException(string message) : base(message) { }
    }

    public class InsufficientCashException : Exception
    {
        public InsufficientCashException(string message) : base(message) { }
    }

    public class DispenseHardwareException : Exception
    {
        public DispenseHardwareException(string message, Exception inner)
            : base(message, inner) { }
    }

    // =====================================================
    // SERVICE INTERFACES
    // =====================================================

    public interface IAccountService
    {
        Task<decimal> GetAvailableBalanceAsync(string accountId);
        Task HoldFundsAsync(string accountId, decimal amount);
        Task ReleaseHoldAsync(string accountId, decimal amount);
        Task DebitAsync(string accountId, decimal amount);
        Task CreditAsync(string accountId, decimal amount);
    }

    public interface ITransactionStore
    {
        Task SaveAsync(AtmTransaction transaction);
        Task UpdateAsync(AtmTransaction transaction);
        Task<AtmTransaction> GetByIdAsync(string transactionId);
        Task<decimal> GetDailyTotalAsync(string accountId, TransactionType type);
    }

    public interface IAuthorizationService
    {
        Task<AuthorizationResult> AuthorizeAsync(AtmTransaction transaction);
    }
}

This implementation demonstrates several critical design patterns:

  • State Machine: The AtmSessionController enforces valid state transitions and prevents invalid operations at each step
  • Transaction Management: Authorization holds funds, completion debits, and void releases — ensuring double-entry accounting integrity
  • Cash Dispensing: The denomination optimizer selects notes while respecting cassette levels and hardware limits, with full revert capability
  • Separation of Concerns: Each class has a single responsibility — card processing, cash dispensing, transaction management, or session control
  • Idempotency: Transaction IDs are deterministic GUIDs, ensuring retries don't create duplicates

25. Conclusion

Designing a production-grade ATM system is one of the most challenging exercises in distributed systems engineering. It requires deep expertise in real-time hardware control, cryptographic security, financial protocols, distributed transactions, and regulatory compliance. The system must operate with near-perfect reliability because it handles real money — errors directly translate to financial losses.

The key architectural decisions we explored include: the session state machine that ensures deterministic ATM behavior, EMV chip processing that prevents card cloning, HSM-based PIN verification that protects authentication credentials, the store-and-forward mechanism that enables offline operation, and the cash management optimization that balances service availability with capital efficiency.

Modern ATMs continue to evolve. We are seeing biometric authentication (fingerprint, facial recognition), AI-powered fraud detection at the edge, cash recycling (deposit and dispense from the same cassette), video banking integration, and even cryptocurrency ATM capabilities. Each new feature adds complexity to the already sophisticated system.

For system design interviews, ATM design tests your ability to reason about stateful sessions, real-time constraints, hardware-software interaction, financial correctness, and graceful degradation — all within a single coherent system. Master these concepts and you will be well-prepared to design any complex real-time distributed system.

Key Takeaways:
  • ATM systems demand strict consistency for financial operations and graceful degradation for availability
  • The session state machine is the core abstraction — every ATM interaction is a state transition
  • Security is not optional — EMV, HSM, DUKPT, and PCI DSS are baseline requirements
  • Cash management is an optimization problem that directly impacts bank profitability
  • Error handling for dispense mismatches and network failures separates production systems from prototypes