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:

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. Start with empty log and pointer files
  2. Feed 50 transactions for ATM 0001
  3. Verify that slots 1 through 50 are populated and slots 51 through 100 are empty
  4. Query ATM 0001 and confirm 50 transactions are displayed in chronological order

Test Scenario 2: Circular Wrap-Around

  1. Feed 150 transactions for ATM 0001 (50 more than the 100-slot buffer)
  2. Verify that the write pointer has wrapped to slot offset 50
  3. Query ATM 0001 and confirm that only the most recent 100 transactions are present
  4. Verify that the oldest 50 transactions have been overwritten

Test Scenario 3: Multiple ATMs

  1. Feed transactions for ATMs 0001, 0500, and 1200 in interleaved order
  2. Verify that each ATM's transactions are in the correct slot range
  3. 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

  1. Feed exactly 247 transactions for ATM 0003
  2. Write pointer should be at offset 47 (247 MOD 100)
  3. 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

  1. 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?

  2. 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?

  3. 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.

  4. 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?

  5. 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?