Case Study 1: ATM Transaction Log Processing
Background
Continental Savings Bank operates a network of 1,200 ATMs across four states. Each ATM generates a continuous stream of transaction records -- balance inquiries, withdrawals, deposits, transfers, and failed authentication attempts. For regulatory compliance and operational monitoring, every transaction must be logged and retained for at least 90 days. The operations team also needs real-time access to the most recent transactions for any given ATM to diagnose hardware problems, investigate fraud alerts, and respond to customer complaints.
The bank's existing transaction logging system writes all ATM transactions to a sequential file. This works well for end-of-day batch processing, but it creates a problem for real-time inquiries: to find the most recent 50 transactions for ATM number 0847, an operator must read through millions of records in the sequential file. During peak hours, this lookup can take several minutes -- far too slow for a customer standing at the teller window asking why the ATM swallowed their card.
The development team has been asked to build a circular transaction log buffer using COBOL relative files. The buffer will hold the most recent N transactions for each ATM, overwriting the oldest entries as new transactions arrive. Because relative files allow direct access by record number, any transaction slot can be read or written in constant time, regardless of how many records the file contains.
Design: The Circular Buffer Concept
A circular buffer (also called a ring buffer) is a fixed-size data structure that reuses storage locations once the buffer is full. The key idea is that new entries overwrite the oldest entries, so the buffer always contains the most recent data.
For the ATM transaction log, each ATM is allocated a fixed block of 100 consecutive record slots in a relative file. ATM number 1 occupies slots 1 through 100, ATM number 2 occupies slots 101 through 200, and so on. Within each ATM's block, a pointer tracks which slot to write next. When the pointer reaches the end of the block, it wraps around to the beginning.
ATM 0001: Slots 1 - 100 (100 transaction slots)
ATM 0002: Slots 101 - 200
ATM 0003: Slots 201 - 300
...
ATM 1200: Slots 119901 - 120000
The formula to calculate the base slot for any ATM is:
base-slot = (ATM-number - 1) * slots-per-ATM + 1
And the actual write slot within the ATM's block is:
write-slot = base-slot + MOD(write-pointer, slots-per-ATM)
This structure gives us O(1) writes (always writing to a calculated slot) and O(N) reads for retrieving all transactions for a specific ATM (where N is the buffer size, a small constant of 100).
Data Design
The Relative File Record Layout
Each transaction log record is fixed at 200 bytes, containing the transaction details plus buffer management fields:
SELECT ATM-LOG-FILE
ASSIGN TO ATMLOGS
ORGANIZATION IS RELATIVE
ACCESS MODE IS DYNAMIC
RELATIVE KEY IS WS-REL-KEY
FILE STATUS IS WS-LOG-STATUS.
FD ATM-LOG-FILE
RECORD CONTAINS 200 CHARACTERS.
01 ATM-LOG-RECORD.
05 ALR-ATM-NUMBER PIC 9(4).
05 ALR-SEQUENCE-NUM PIC 9(6).
05 ALR-TIMESTAMP.
10 ALR-TXN-DATE PIC 9(8).
10 ALR-TXN-TIME PIC 9(6).
05 ALR-TXN-TYPE PIC X(2).
88 ALR-BALANCE-INQ VALUE "BI".
88 ALR-WITHDRAWAL VALUE "WD".
88 ALR-DEPOSIT VALUE "DP".
88 ALR-TRANSFER VALUE "TF".
88 ALR-PIN-FAILURE VALUE "PF".
88 ALR-CARD-RETAINED VALUE "CR".
88 ALR-REVERSAL VALUE "RV".
05 ALR-ACCOUNT-NUMBER PIC X(12).
05 ALR-CARD-NUMBER PIC X(16).
05 ALR-TXN-AMOUNT PIC S9(9)V99.
05 ALR-BALANCE-AFTER PIC S9(9)V99.
05 ALR-RESPONSE-CODE PIC X(2).
05 ALR-TERMINAL-ID PIC X(8).
05 ALR-SLOT-ACTIVE-FLAG PIC X(1).
88 ALR-SLOT-ACTIVE VALUE "A".
88 ALR-SLOT-EMPTY VALUE "E".
05 ALR-FILLER PIC X(119).
The ATM Pointer Table
A separate indexed file maintains the current write pointer for each ATM. This pointer indicates which slot within the ATM's 100-slot block will receive the next transaction:
SELECT ATM-POINTER-FILE
ASSIGN TO ATMPTRS
ORGANIZATION IS INDEXED
ACCESS MODE IS RANDOM
RECORD KEY IS APR-ATM-NUMBER
FILE STATUS IS WS-PTR-STATUS.
FD ATM-POINTER-FILE
RECORD CONTAINS 30 CHARACTERS.
01 ATM-POINTER-RECORD.
05 APR-ATM-NUMBER PIC 9(4).
05 APR-WRITE-POINTER PIC 9(3).
05 APR-TOTAL-TXN-COUNT PIC 9(8).
05 APR-LAST-TXN-DATE PIC 9(8).
05 APR-LAST-TXN-TIME PIC 9(6).
05 APR-FILLER PIC X(1).
Working-Storage Variables for Slot Calculation
WORKING-STORAGE SECTION.
01 WS-REL-KEY PIC 9(6).
01 WS-LOG-STATUS PIC XX.
01 WS-PTR-STATUS PIC XX.
01 WS-BUFFER-CONSTANTS.
05 WS-SLOTS-PER-ATM PIC 9(3) VALUE 100.
05 WS-MAX-ATMS PIC 9(4) VALUE 1200.
01 WS-SLOT-CALC.
05 WS-BASE-SLOT PIC 9(6).
05 WS-CURRENT-OFFSET PIC 9(3).
05 WS-TARGET-SLOT PIC 9(6).
Core Processing Logic
Writing a Transaction to the Circular Buffer
When a new transaction arrives from the ATM network feed, the program calculates the target slot and writes the record:
2000-LOG-TRANSACTION.
* Read current write pointer for this ATM
MOVE IN-ATM-NUMBER TO APR-ATM-NUMBER
READ ATM-POINTER-FILE INTO ATM-POINTER-RECORD
INVALID KEY
PERFORM 2100-INIT-NEW-ATM
NOT INVALID KEY
CONTINUE
END-READ
* Calculate the target slot number
COMPUTE WS-BASE-SLOT =
(IN-ATM-NUMBER - 1) * WS-SLOTS-PER-ATM + 1
MOVE APR-WRITE-POINTER TO WS-CURRENT-OFFSET
COMPUTE WS-TARGET-SLOT =
WS-BASE-SLOT + WS-CURRENT-OFFSET
MOVE WS-TARGET-SLOT TO WS-REL-KEY
* Build the log record
MOVE IN-ATM-NUMBER TO ALR-ATM-NUMBER
ADD 1 TO APR-TOTAL-TXN-COUNT
MOVE APR-TOTAL-TXN-COUNT TO ALR-SEQUENCE-NUM
MOVE WS-CURRENT-DATE TO ALR-TXN-DATE
MOVE WS-CURRENT-TIME TO ALR-TXN-TIME
MOVE IN-TXN-TYPE TO ALR-TXN-TYPE
MOVE IN-ACCOUNT-NUMBER TO ALR-ACCOUNT-NUMBER
MOVE IN-CARD-NUMBER TO ALR-CARD-NUMBER
MOVE IN-TXN-AMOUNT TO ALR-TXN-AMOUNT
MOVE IN-BALANCE-AFTER TO ALR-BALANCE-AFTER
MOVE IN-RESPONSE-CODE TO ALR-RESPONSE-CODE
MOVE IN-TERMINAL-ID TO ALR-TERMINAL-ID
SET ALR-SLOT-ACTIVE TO TRUE
* Write (or rewrite) the record at the target slot
REWRITE ATM-LOG-RECORD
INVALID KEY
WRITE ATM-LOG-RECORD
INVALID KEY
PERFORM 9100-LOG-WRITE-ERROR
END-WRITE
END-REWRITE
* Advance the write pointer (wrap around at 100)
ADD 1 TO APR-WRITE-POINTER
IF APR-WRITE-POINTER >= WS-SLOTS-PER-ATM
MOVE 0 TO APR-WRITE-POINTER
END-IF
* Update pointer record
MOVE WS-CURRENT-DATE TO APR-LAST-TXN-DATE
MOVE WS-CURRENT-TIME TO APR-LAST-TXN-TIME
REWRITE ATM-POINTER-RECORD
INVALID KEY
PERFORM 9200-POINTER-WRITE-ERROR
END-REWRITE
.
Initializing a New ATM Entry
When a transaction arrives from an ATM that has never logged before, the program initializes both the pointer record and the 100 empty slots:
2100-INIT-NEW-ATM.
MOVE IN-ATM-NUMBER TO APR-ATM-NUMBER
MOVE 0 TO APR-WRITE-POINTER
MOVE 0 TO APR-TOTAL-TXN-COUNT
MOVE SPACES TO APR-LAST-TXN-DATE
MOVE SPACES TO APR-LAST-TXN-TIME
WRITE ATM-POINTER-RECORD
INVALID KEY
PERFORM 9200-POINTER-WRITE-ERROR
END-WRITE
* Pre-allocate all 100 slots as empty
COMPUTE WS-BASE-SLOT =
(IN-ATM-NUMBER - 1) * WS-SLOTS-PER-ATM + 1
INITIALIZE ATM-LOG-RECORD
SET ALR-SLOT-EMPTY TO TRUE
MOVE IN-ATM-NUMBER TO ALR-ATM-NUMBER
PERFORM VARYING WS-CURRENT-OFFSET
FROM 0 BY 1
UNTIL WS-CURRENT-OFFSET >= WS-SLOTS-PER-ATM
COMPUTE WS-REL-KEY =
WS-BASE-SLOT + WS-CURRENT-OFFSET
WRITE ATM-LOG-RECORD
INVALID KEY
PERFORM 9100-LOG-WRITE-ERROR
END-WRITE
END-PERFORM
.
Retrieving Recent Transactions for an ATM
The inquiry function reads all active slots for a given ATM and presents them in chronological order. Because the circular buffer overwrites the oldest records first, the program must read starting from the current write pointer (the oldest surviving record) and wrap around:
3000-INQUIRY-BY-ATM.
MOVE INQ-ATM-NUMBER TO APR-ATM-NUMBER
READ ATM-POINTER-FILE INTO ATM-POINTER-RECORD
INVALID KEY
DISPLAY "ATM " INQ-ATM-NUMBER
" NOT FOUND IN LOG"
GO TO 3000-EXIT
END-READ
COMPUTE WS-BASE-SLOT =
(INQ-ATM-NUMBER - 1) * WS-SLOTS-PER-ATM + 1
* Start reading from the oldest record (write pointer)
* and wrap around through all 100 slots
MOVE 0 TO WS-DISPLAY-COUNT
PERFORM VARYING WS-READ-INDEX
FROM 0 BY 1
UNTIL WS-READ-INDEX >= WS-SLOTS-PER-ATM
COMPUTE WS-CURRENT-OFFSET = FUNCTION MOD(
APR-WRITE-POINTER + WS-READ-INDEX
WS-SLOTS-PER-ATM)
COMPUTE WS-REL-KEY =
WS-BASE-SLOT + WS-CURRENT-OFFSET
READ ATM-LOG-FILE INTO ATM-LOG-RECORD
INVALID KEY
CONTINUE
NOT INVALID KEY
IF ALR-SLOT-ACTIVE
ADD 1 TO WS-DISPLAY-COUNT
PERFORM 3100-FORMAT-TXN-LINE
END-IF
END-READ
END-PERFORM
DISPLAY "TOTAL TRANSACTIONS DISPLAYED: "
WS-DISPLAY-COUNT
.
3000-EXIT.
EXIT.
The Batch Feed Program: End-to-End Flow
The complete batch program reads transactions from the ATM network feed (a sequential input file), logs each one to the circular buffer, and produces a summary report:
IDENTIFICATION DIVISION.
PROGRAM-ID. ATMLGPRC.
PROCEDURE DIVISION.
0000-MAIN.
PERFORM 1000-INITIALIZE
PERFORM 2000-LOG-TRANSACTION
UNTIL WS-INPUT-EOF
PERFORM 8000-PRODUCE-SUMMARY
PERFORM 9000-TERMINATE
STOP RUN
.
1000-INITIALIZE.
OPEN INPUT ATM-FEED-FILE
OPEN I-O ATM-LOG-FILE
OPEN I-O ATM-POINTER-FILE
OPEN OUTPUT SUMMARY-REPORT
MOVE FUNCTION CURRENT-DATE TO WS-SYSTEM-DATETIME
MOVE WS-SYS-DATE TO WS-CURRENT-DATE
MOVE WS-SYS-TIME TO WS-CURRENT-TIME
MOVE 0 TO WS-TOTAL-TXN-READ
MOVE 0 TO WS-TOTAL-TXN-LOGGED
MOVE 0 TO WS-TOTAL-ERRORS
PERFORM 1100-READ-FEED
.
9000-TERMINATE.
CLOSE ATM-FEED-FILE
CLOSE ATM-LOG-FILE
CLOSE ATM-POINTER-FILE
CLOSE SUMMARY-REPORT
.
JCL for the Batch Log Processing Job
//ATMLGPRC JOB (ACCT),'ATM LOG PROCESS',
// CLASS=A,MSGCLASS=X,NOTIFY=&SYSUID
//*
//STEP01 EXEC PGM=ATMLGPRC
//STEPLIB DD DSN=PROD.LOADLIB,DISP=SHR
//*
//* ATM NETWORK TRANSACTION FEED - SEQUENTIAL INPUT
//ATMFEED DD DSN=ATM.DAILY.FEED(0),DISP=SHR
//*
//* RELATIVE FILE - CIRCULAR TRANSACTION LOG BUFFER
//* VSAM RRDS ALLOCATED WITH 120000 RECORDS (1200 ATMS * 100 SLOTS)
//ATMLOGS DD DSN=ATM.LOG.BUFFER,DISP=SHR
//*
//* INDEXED FILE - ATM WRITE POINTERS
//ATMPTRS DD DSN=ATM.POINTER.TABLE,DISP=SHR
//*
//* SUMMARY REPORT OUTPUT
//SUMMARY DD SYSOUT=*
//*
//SYSOUT DD SYSOUT=*
//SYSUDUMP DD SYSOUT=*
Operational Inquiry Program
A separate online inquiry program allows operators to look up recent transactions for any ATM. This program opens the relative file in INPUT mode with DYNAMIC access so it can both calculate specific slot positions for random reads and optionally scan sequentially through an ATM's block:
IDENTIFICATION DIVISION.
PROGRAM-ID. ATMLGINQ.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT ATM-LOG-FILE
ASSIGN TO ATMLOGS
ORGANIZATION IS RELATIVE
ACCESS MODE IS DYNAMIC
RELATIVE KEY IS WS-REL-KEY
FILE STATUS IS WS-LOG-STATUS.
PROCEDURE DIVISION.
0000-MAIN.
OPEN INPUT ATM-LOG-FILE
OPEN INPUT ATM-POINTER-FILE
DISPLAY "ENTER ATM NUMBER (0000 TO QUIT): "
ACCEPT INQ-ATM-NUMBER
PERFORM UNTIL INQ-ATM-NUMBER = ZEROS
PERFORM 3000-INQUIRY-BY-ATM
DISPLAY " "
DISPLAY "ENTER ATM NUMBER (0000 TO QUIT): "
ACCEPT INQ-ATM-NUMBER
END-PERFORM
CLOSE ATM-LOG-FILE
CLOSE ATM-POINTER-FILE
STOP RUN
.
Why Relative Files Are the Right Choice Here
This design exploits the core strengths of relative files:
-
O(1) writes: Every transaction is written to a calculated slot number. There is no index to traverse, no sequential scan, and no insert-in-order logic. The VSAM RRDS subsystem computes the physical disk address from the record number using simple arithmetic.
-
O(1) reads by position: When an operator requests ATM 0847's recent transactions, the program computes the base slot (84601) and reads slots 84601 through 84700. Each read is a direct disk access.
-
Fixed storage footprint: The file never grows. At 200 bytes per record and 120,000 total slots, the file occupies exactly 24 MB. This predictability is valuable in a mainframe environment where disk allocation must be planned in advance.
-
Natural circular buffer semantics: The relative record number directly maps to the buffer slot position. The modular arithmetic for wrapping is trivial. An indexed file would require maintaining a separate key structure, and a sequential file would require periodic truncation.
An indexed file could achieve similar read performance for key-based lookups, but it would require maintaining an index structure and would not naturally support the circular overwrite pattern. A sequential file would support the append pattern but would not support random reads. The relative file combines the best characteristics of both for this specific use case.
Testing the System
Test Scenario 1: Initial Population
- Start with empty log and pointer files
- Feed 50 transactions for ATM 0001
- Verify that slots 1 through 50 are populated and slots 51 through 100 are empty
- Query ATM 0001 and confirm 50 transactions are displayed in chronological order
Test Scenario 2: Circular Wrap-Around
- Feed 150 transactions for ATM 0001 (50 more than the 100-slot buffer)
- Verify that the write pointer has wrapped to slot offset 50
- Query ATM 0001 and confirm that only the most recent 100 transactions are present
- Verify that the oldest 50 transactions have been overwritten
Test Scenario 3: Multiple ATMs
- Feed transactions for ATMs 0001, 0500, and 1200 in interleaved order
- Verify that each ATM's transactions are in the correct slot range
- Confirm that ATM 0001 uses slots 1-100, ATM 0500 uses slots 49901-50000, and ATM 1200 uses slots 119901-120000
Test Scenario 4: Inquiry After Wrap-Around
- Feed exactly 247 transactions for ATM 0003
- Write pointer should be at offset 47 (247 MOD 100)
- Query ATM 0003 and verify transactions are displayed in chronological order, starting from the oldest surviving transaction and wrapping through the buffer
Lessons Learned
1. Pre-Allocating Slots Prevents INVALID KEY on REWRITE
The initial design attempted to REWRITE records without first creating them, which produced INVALID KEY errors on empty slots. Pre-allocating all slots during ATM initialization solved this issue and also allowed the inquiry program to distinguish between "empty" and "overwritten" slots using the active flag.
2. DYNAMIC Access Mode Is Essential
The program needs both random access (for writing to calculated slots) and sequential access (for scanning an ATM's block during inquiries). The DYNAMIC access mode provides both capabilities with a single file declaration, avoiding the need to close and reopen the file with different access modes.
3. Pointer Persistence Requires Careful Error Handling
If the pointer file update fails after the log record is written, the pointer and the actual file state become inconsistent. The production version includes a verification step that reads back the written record and confirms it matches before updating the pointer.
4. The Slot Calculation Must Be Bulletproof
An off-by-one error in the base slot calculation would cause one ATM's transactions to overwrite another ATM's data. The team added assertions that verify the calculated slot falls within the expected range for the given ATM number before every write operation.
Source Code
The complete implementation is available in code/case-study-code.cob and code/case-study-code.jcl. Compile and run with GnuCOBOL:
cobc -x case-study-code.cob
./case-study-code
Discussion Questions
-
What would happen if two batch jobs tried to update the same ATM's log buffer simultaneously? What z/OS mechanisms could prevent this, and how would the COBOL program need to change?
-
The current design allocates 100 slots per ATM regardless of how busy the ATM is. How would you modify the design to give busy ATMs more slots while keeping the total file size fixed?
-
If the bank adds 300 new ATMs, the file must be expanded from 120,000 to 150,000 slots. Describe the steps needed to resize the VSAM RRDS without losing existing data.
-
Why does the inquiry program read slots starting from the write pointer rather than from the base slot? What would go wrong if it started from the base slot?
-
How would this design change if the retention requirement increased from "most recent 100 transactions" to "all transactions from the last 7 days"? Would relative files still be the best choice?