How to Design an ATM System
A Senior+ Guide — Building cash dispensing, card processing, transaction management, and fleet monitoring for 3M+ ATMs worldwide
Table of Contents
- Introduction — 3M+ ATMs Globally
- Functional & Non-Functional Requirements
- Capacity Estimation
- Data Model
- API Design
- High-Level Architecture
- ATM Terminal Hardware
- Session State Machine
- Card Processing & EMV
- Cash Dispensing Logic
- PIN Verification
- Transaction Processing
- Cash Management & Replenishment
- Network Communication
- Error Handling & Recovery
- Anti-Skimming & Security
- Multi-Bank & Interoperability
- Deposit Processing
- Database Design
- Caching Strategy
- Multi-Region Design
- Cost Estimation
- Interview Q&A
- Full C# Implementation
- Conclusion
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.
2. Functional & Non-Functional Requirements
Functional Requirements
| # | Requirement | Description | Priority |
|---|---|---|---|
| FR-01 | Cash Withdrawal | User withdraws cash from linked account with denomination selection | P0 — Critical |
| FR-02 | Cash Deposit | User deposits cash or checks via envelope-free deposit with image capture | P0 — Critical |
| FR-03 | Balance Inquiry | User checks account balance and available balance | P0 — Critical |
| FR-04 | PIN Change | User changes PIN with old PIN verification | P1 — High |
| FR-05 | Fund Transfer | Transfer between linked accounts at same bank | P1 — High |
| FR-06 | Mini Statement | Print last 10 transactions on receipt | P1 — High |
| FR-07 | Cardless Withdrawal | QR code or one-time code based cash withdrawal | P2 — Medium |
| FR-08 | Bill Payment | Utility bill payment through ATM | P2 — Medium |
| FR-09 | Multi-Language | Support 3+ languages per region | P1 — High |
| FR-10 | Receipt Printing | Print or email transaction receipt | P1 — High |
Non-Functional Requirements
| # | Requirement | Target | Justification |
|---|---|---|---|
| NFR-01 | Availability | 99.95% uptime | ATMs must be available 24/7; planned maintenance windows |
| NFR-02 | Latency | < 30 seconds end-to-end | Customer patience threshold; card timeout limits |
| NFR-03 | Throughput | 100+ transactions/second per region | Peak lunch-hour and month-end salary volumes |
| NFR-04 | Security | PCI DSS Level 1 compliance | Mandatory for card processing; zero-tolerance for breaches |
| NFR-05 | Durability | 15-year hardware lifecycle | ATMs are deployed for extended periods |
| NFR-06 | Fault Tolerance | Graceful degradation | Partial functionality if one module fails |
| NFR-07 | Audit Trail | 7-year retention | Financial regulatory compliance (SOX, PCI, local banking laws) |
| NFR-08 | Offline Mode | Store-and-forward for network outages | ATMs 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.
| Metric | Calculation | Result |
|---|---|---|
| Total ATMs | Direct deployment | 50,000 |
| Avg transactions/ATM/day | Industry average for urban areas | 250 |
| Daily transactions | 50,000 × 250 | 12,500,000 |
| Peak transactions/hour | 15% of daily in 1 hour (lunch peak) | 1,875,000 |
| Peak transactions/second | 1,875,000 / 3600 | ~520 TPS |
| Avg withdrawal amount | Regional average | $200 |
| Daily cash dispensed | 12.5M × $200 (assuming 80% withdrawals) | $2 billion |
| Avg cash per ATM | $40,000 loaded | $2 billion total float |
| Data per transaction | ISO 8583 message ~1KB + audit ~0.5KB | 1.5 KB |
| Daily storage | 12.5M × 1.5 KB | ~19 GB/day |
| Annual storage | 19 GB × 365 | ~7 TB/year |
| Network bandwidth per ATM | 50 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.
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.
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)
| Endpoint | Method | Description | Key Parameters |
|---|---|---|---|
/api/v1/terminal/heartbeat | POST | Periodic status signal from ATM | atm_id, status, cassette_levels[], software_version |
/api/v1/card/insert | POST | Card inserted event, begin session | atm_id, card_track2_encrypted, reader_type |
/api/v1/card/pin/verify | POST | Submit encrypted PIN block for verification | session_id, encrypted_pin_block, pin_length |
/api/v1/transaction/withdraw | POST | Cash withdrawal authorization request | session_id, account_id, amount, denomination_prefs |
/api/v1/transaction/deposit | POST | Cash or check deposit | session_id, account_id, deposit_type, images[] |
/api/v1/transaction/balance | GET | Account balance inquiry | session_id, account_id |
/api/v1/transaction/statement | GET | Mini statement (last N transactions) | session_id, account_id, count |
/api/v1/transaction/complete | POST | Confirm transaction completion (cash taken) | session_id, transaction_id, dispense_status |
/api/v1/card/eject | POST | Card returned to customer, session end | session_id, reason |
Management APIs (Backend → ATM Fleet)
| Endpoint | Method | Description |
|---|---|---|
/api/v1/fleet/status | GET | Get status of all ATMs or filtered subset |
/api/v1/fleet/{atm_id}/config | PUT | Push configuration update to specific ATM |
/api/v1/fleet/{atm_id}/reboot | POST | Remote reboot command |
/api/v1/fleet/{atm_id}/cash-forecast | GET | Get cash replenishment forecast |
/api/v1/fleet/alerts | GET | Active alerts across the fleet |
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.
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.
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
| Component | Interface | Data Rate | Failure Mode | Software Impact |
|---|---|---|---|---|
| Card Reader (Motorized) | RS-232 / USB | 9600 bps | Card jam, read failure | Eject card, log event, alert fleet mgmt |
| Card Reader (Contactless) | ISO 14443 / NFC | 424 kbps | Tag read failure, interference | Fallback to swipe/chip, retry 3x |
| Encrypting PIN Pad | Internal bus (encrypted) | — | Tamper detected, self-destruct | Shut down all PIN operations immediately |
| Cash Dispenser | CCDM protocol | 9600 bps | Shutter jam, pick sensor, note sensor | Abort dispense, void transaction, reverse hold |
| Receipt Printer | ESC/POS over RS-232 | 19200 bps | Paper out, print head failure | Email receipt fallback, log warning |
| Deposit Module | Proprietary + image SDK | USB 2.0 | Scanner jam, image quality failure | Reject deposit, ask user to retry |
| PIN Camera | USB / IP camera | 720p stream | Lens obstruction, failure | Log alert, continue transaction (non-critical) |
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.
Session Timeout Configuration
| State | Timeout | Action on Timeout |
|---|---|---|
| IDLE | None (infinite) | Return to screensaver |
| CARD_INSERTED | 10 seconds | Eject card, log event |
| PIN_ENTRY | 30 seconds | Eject card, log event |
| MENU_DISPLAYED | 60 seconds | Eject card, cancel session |
| AMOUNT_ENTRY | 30 seconds | Return to menu |
| AUTHORIZING | 30 seconds | Decline, return to menu |
| DISPENSING | 15 seconds | Hardware retry, then void |
| CASH_PRESENTED | 30 seconds | Retract cash, reverse transaction |
| DEPOSIT_ACCEPTING | 60 seconds | Return items, cancel deposit |
| CARD_EJECTING | 10 seconds | Retract 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
Card Interface Types
| Interface | Standard | Data | Security | Usage at ATM |
|---|---|---|---|---|
| EMV Chip (Contact) | ISO 7816 | Full card data, cryptograms | Highest — dynamic auth per txn | Primary method, ~85% of transactions |
| Magnetic Stripe | ISO 7811 | Static Track 1 + Track 2 | Low — skimmable, no dynamic auth | Fallback only, being phased out |
| Contactless NFC | ISO 14443 | Same as chip via radio | Same as chip, limited amount offline | Growing — 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.
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.
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
| Cassette | Position | Capacity | Low Threshold | Refill Trigger |
|---|---|---|---|---|
| Cassette 1 | Top | 2,000 notes ($20) | 200 notes | Below 200 → alert CIT crew |
| Cassette 2 | Upper-Mid | 2,000 notes ($50) | 150 notes | Below 150 → alert CIT crew |
| Cassette 3 | Lower-Mid | 2,000 notes ($100) | 100 notes | Below 100 → alert CIT crew |
| Cassette 4 | Bottom | 1,000 notes ($10) | 100 notes | Below 100 → alert CIT crew |
| Deposit Cassette | Separate | 5,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.
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
| Format | Standard | Structure | Usage |
|---|---|---|---|
| ISO 9564 Format 0 | ISO 9564-1 | XOR of PIN (padded) + PAN (truncated) | Legacy ATMs, most common globally |
| ISO 9564 Format 1 | ISO 9564-1 | Random key + encrypted PIN | Higher security environments |
| ISO 9564 Format 3 | ISO 9564-1 | Random key + encrypted (PIN length + PIN) | Recommended for new deployments |
| ISO 9564 Format 4 | ISO 9564-1 | AES-128 encrypted PIN block | Future-proof, AES-based |
PIN Verification Methods
- 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.
- 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.
- 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.
Transaction Lifecycle States
| State | Description | Account Impact | Reversible? |
|---|---|---|---|
| PENDING | Transaction created, authorization in progress | None | Yes (just delete) |
| AUTHORIZED | Funds held, awaiting execution | Balance hold (available reduced) | Yes (release hold) |
| COMPLETED | Cash dispensed / deposit accepted | Balance debited / credited | Yes (reversal transaction) |
| SETTLED | Posted to ledger, included in settlement | Final — included in bank's books | Yes (but complex — needs adjustment) |
| DECLINED | Authorization denied | None | N/A |
| REVERSED | Completed transaction was reversed | Reversed — funds returned | No (creates new reversal) |
| VOIDED | Transaction cancelled before completion | Hold released | N/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.
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
| Strategy | Description | Best For | Cost |
|---|---|---|---|
| Fixed Schedule | Replenish every N days regardless of level | Low-volume ATMs in stable areas | Low (predictable CIT costs) |
| Threshold-Based | Replenish when cassette falls below threshold | Medium-volume ATMs | Medium (responsive but unpredictable) |
| Demand-Predicted | ML model predicts when replenishment needed | High-volume, variable-demand ATMs | Optimized (lowest total cost) |
| Hybrid | Predicted with threshold safety net | Most real-world deployments | Balanced |
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.
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
| Mechanism | When Used | Account Impact | Complexity |
|---|---|---|---|
| Void | Transaction authorized but not yet completed | Release hold, no debit | Low |
| Reversal | Transaction completed but needs undoing | Reverse debit, credit back | Medium |
| Adjustment | Manual correction by bank operations | Manual debit/credit | High (requires approval) |
Dispense Mismatch Handling
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
| Technology | How It Works | Protection Against |
|---|---|---|
| Jitter Card Reader | Randomizes card insertion speed to corrupt skimmer data | Magnetic stripe skimmers |
| EMV-Only Mode | Disables magnetic stripe reading entirely | All stripe-based skimming |
| Card Shimmer Detection | Monitors ICC communication for anomalies; shim is a thin device in the chip slot | Chip data interception (shimmers) |
| Camera Detection | IR sensors detect unauthorized cameras near PIN pad | PIN shoulder surfing / cameras |
| Tamper-Evident Bezels | Overlay detects physical removal attempts | Physical overlay attacks |
| Geolocation Validation | Verifies ATM is at its registered location | ATM relocation attacks |
| Vibration Sensors | Detect drilling or physical tampering | Cash 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.
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.
| Network | Type | Coverage | Interchange Fee (typical) |
|---|---|---|---|
| Visa PLUS | Global | 200+ countries | $0.50 - $2.50 per transaction |
| Mastercard Cirrus | Global | 200+ 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
- Customer selects deposit and places a stack of notes in the acceptor slot
- The deposit module feeds notes one at a time through the validation path
- Each note is scanned for denomination (UV, magnetic, and optical sensors), authenticity (counterfeit detection), and fitness (damaged notes rejected)
- The validated count and total are displayed to the customer for confirmation
- Customer confirms — notes are moved from the staging area to the deposit cassette
- Account is credited immediately (provisional) or after backend processing (deferred)
- 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 Key | Data | TTL | Invalidation | Stale Policy |
|---|---|---|---|---|
| balance:{account_id} | Available balance | 30 seconds | On every transaction | Never serve stale balance |
| config:{atm_id} | ATM configuration, limits, denominations | 5 minutes | On config push | Serve stale if backend unreachable |
| card:{card_token_hash} | Card status, linked accounts | 60 seconds | On status change | Never serve stale card status |
| limits:{account_id} | Daily withdrawal/deposit limits | 60 seconds | On transaction | Conservative: assume limit reached |
| fraud_score:{card_token} | Current fraud risk score | 5 minutes | On new score calculation | Serve stale, flag for review |
| exchange_rate:{currency} | Forex rates for multi-currency | 15 minutes | Scheduled refresh | Serve stale with rate advisory |
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.
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 Component | Per Unit (Annual) | Fleet (50K ATMs) | Notes |
|---|---|---|---|
| ATM Hardware (amortized) | $3,000 | $150M | 5-year lifecycle for $15K machine |
| ATM Site Lease | $6,000 - $24,000 | $600M | Varies 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 | $250M | Includes armored vehicles, personnel, insurance |
| Network Connectivity | $1,200 | $60M | VPN, dual connections for redundancy |
| Software Licenses | $2,000 | $100M | Switch software, monitoring, security |
| Maintenance & Repairs | $3,000 | $150M | Parts, technician dispatches |
| PCI DSS Compliance | $500 | $25M | Audits, penetration testing, certifications |
| Interchange Fees | $0.50 - $2.50 per txn | $75M | Paid on interbank transactions |
| Cloud Infrastructure | $500 | $25M | Backend servers, databases, monitoring |
| Total Annual Operating Cost | $15K - $40K per ATM | $935M - $985M | Excluding 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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
AtmSessionControllerenforces 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.
- 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