> "The mainframe does not need to be an island. It just needs a bridge."
In This Chapter
- 39.1 The Integration Challenge
- 39.2 IBM MQ: Message Queuing from COBOL
- 39.3 JSON Processing in COBOL
- 39.4 XML Processing in COBOL
- 39.5 REST APIs from COBOL
- 39.6 Event-Driven Patterns
- 39.7 Data Transformation Patterns
- 39.8 GlobalBank: Real-Time Balance Inquiry API
- 39.9 MedClaim: Real-Time Eligibility Check
- 39.10 API Gateway Integration
- 39.11 Error Handling in Integration
- 39.12 Try It Yourself: Building an MQ Message Processor
- 39.13 Performance Considerations for Integration
- 39.14 MQ Message Formats and Headers in Depth
- 39.15 JSON PARSE Error Handling in Detail
- 39.16 XML GENERATE Patterns
- 39.17 Event Sourcing Concepts for COBOL Developers
- 39.18 Try It Yourself: JSON Round-Trip Exercise
- 39.19 MQ Dead Letter Queue Management
- 39.20 MedClaim: Event-Driven Claims Status Notification
- 39.21 Integration Security Patterns
- 39.22 Performance Tuning for High-Volume Integration
- 39.24 Chapter Summary
Chapter 39: Real-Time Integration
"The mainframe does not need to be an island. It just needs a bridge." — Priya Kapoor, designing GlobalBank's API gateway architecture
For decades, the knock on COBOL was simple: it lives on the mainframe, and the mainframe is a closed box. Transactions go in through 3270 terminals, batch files come out on tapes and print queues, and everything in between is invisible to the outside world. That characterization was never entirely accurate, but it was close enough to make COBOL systems feel isolated from the web-based, API-driven, event-streaming world that emerged around them.
That era is over.
Modern COBOL interacts with the rest of the technology stack through message queues, web services, REST APIs, JSON, and XML. The COBOL programs themselves have not fundamentally changed — they still process records, apply business rules, and manage data with the same rigor they always have. What has changed is the plumbing that connects them. A COBOL program that once received input exclusively from a CICS screen or a batch file can now receive a JSON payload from a mobile app, process it, and return a JSON response — all within a few hundred milliseconds.
This chapter teaches you the integration patterns that make this possible. By the end, you will know how to send and receive messages through IBM MQ, parse and generate JSON and XML in COBOL, consume and provide REST APIs, and implement event-driven architectures with COBOL at the core.
39.1 The Integration Challenge
Before we dive into technology, let us understand the fundamental challenge. COBOL programs were designed around:
- Fixed-length records with precisely defined copybook layouts
- EBCDIC character encoding (on mainframes)
- Packed decimal and zoned decimal numeric representations
- Synchronous, procedural execution models
The modern world operates on:
- Variable-length messages in JSON or XML
- ASCII or UTF-8 encoding
- IEEE floating-point and string-based numbers
- Asynchronous, event-driven patterns
Every integration pattern in this chapter addresses the gap between these two worlds. The good news: IBM and the COBOL language itself have evolved to handle these translations natively. You do not need middleware frameworks or custom code for the basics — the compiler and runtime provide built-in support.
💡 Key Insight: Integration does not mean rewriting. The COBOL business logic stays the same. What changes is the data format at the boundary — how data enters and leaves the COBOL program. Think of integration as putting new doors on an existing building.
39.2 IBM MQ: Message Queuing from COBOL
IBM MQ (formerly MQSeries, then WebSphere MQ) is the most widely used message queuing system in the enterprise world. It enables applications written in different languages, running on different platforms, to communicate reliably through messages placed on queues.
Message Queuing Concepts
A message queue is like a mailbox. One program puts a message in; another program takes it out. The two programs do not need to run at the same time or even on the same machine. This decoupling is what makes MQ so powerful for integration.
Key concepts: - Queue Manager: The MQ server that hosts queues and manages connections - Queue: A named destination where messages are stored - Message: A unit of data with a header (metadata) and a body (payload) - Channel: A communication path between queue managers - MQPUT: The API call to put a message on a queue - MQGET: The API call to get a message from a queue - MQOPEN / MQCLOSE: Open and close a queue for access
MQ from COBOL: The API
COBOL programs call MQ through the MQI (Message Queue Interface). The API uses standard CALL statements with MQ copybooks defining the data structures.
WORKING-STORAGE SECTION.
* MQ API data structures (from IBM copybooks)
COPY CMQV.
* Contains MQ constants like MQOO-INPUT-SHARED,
* MQMD-DEFAULT, etc.
01 WS-HCONN PIC S9(09) BINARY.
01 WS-HOBJ PIC S9(09) BINARY.
01 WS-COMPCODE PIC S9(09) BINARY.
01 WS-REASON PIC S9(09) BINARY.
01 WS-OPTIONS PIC S9(09) BINARY.
01 WS-BUFFER-LENGTH PIC S9(09) BINARY.
01 WS-DATA-LENGTH PIC S9(09) BINARY.
* MQ Object Descriptor
01 WS-OBJ-DESC.
COPY CMQODV.
* MQ Message Descriptor
01 WS-MSG-DESC.
COPY CMQMDV.
* MQ Put Message Options
01 WS-PMO.
COPY CMQPMOV.
* MQ Get Message Options
01 WS-GMO.
COPY CMQGMOV.
* Message buffer
01 WS-MSG-BUFFER PIC X(4096).
Connecting to the Queue Manager
1100-CONNECT-TO-MQ.
* -----------------------------------------------
* Connect to the queue manager
* On z/OS with CICS, this is often implicit
* In batch, use explicit MQCONN
* -----------------------------------------------
CALL 'MQCONN'
USING WS-QMGR-NAME
WS-HCONN
WS-COMPCODE
WS-REASON
IF WS-COMPCODE NOT = MQCC-OK
DISPLAY 'MQ CONNECT FAILED'
DISPLAY ' Completion code: ' WS-COMPCODE
DISPLAY ' Reason code: ' WS-REASON
MOVE 16 TO RETURN-CODE
STOP RUN
END-IF
DISPLAY 'Connected to queue manager '
WS-QMGR-NAME.
Putting a Message on a Queue
2100-SEND-MESSAGE.
* -----------------------------------------------
* Open the output queue
* -----------------------------------------------
MOVE MQOT-Q TO MQOD-OBJECTTYPE
MOVE 'GBANK.TXN.NOTIFY' TO MQOD-OBJECTNAME
MOVE MQOO-OUTPUT TO WS-OPTIONS
CALL 'MQOPEN'
USING WS-HCONN
WS-OBJ-DESC
WS-OPTIONS
WS-HOBJ
WS-COMPCODE
WS-REASON
IF WS-COMPCODE NOT = MQCC-OK
DISPLAY 'MQOPEN failed, reason=' WS-REASON
PERFORM 9800-MQ-ERROR
END-IF
* -----------------------------------------------
* Build the message content
* -----------------------------------------------
INITIALIZE WS-MSG-BUFFER
STRING '{"accountNo":"' WS-ACCT-NUMBER '",'
'"txnType":"' WS-TXN-TYPE '",'
'"amount":' WS-TXN-AMOUNT-DISP ','
'"timestamp":"' WS-TIMESTAMP '"}'
DELIMITED SIZE
INTO WS-MSG-BUFFER
POINTER WS-MSG-LENGTH
SUBTRACT 1 FROM WS-MSG-LENGTH
MOVE WS-MSG-LENGTH TO WS-BUFFER-LENGTH
* -----------------------------------------------
* Set message descriptor properties
* -----------------------------------------------
MOVE MQMD-DEFAULT TO WS-MSG-DESC
MOVE MQMT-DATAGRAM TO MQMD-MSGTYPE
MOVE MQPER-PERSISTENT TO MQMD-PERSISTENCE
MOVE MQFMT-STRING TO MQMD-FORMAT
* -----------------------------------------------
* Put the message
* -----------------------------------------------
MOVE MQPMO-DEFAULT TO WS-PMO
CALL 'MQPUT'
USING WS-HCONN
WS-HOBJ
WS-MSG-DESC
WS-PMO
WS-BUFFER-LENGTH
WS-MSG-BUFFER
WS-COMPCODE
WS-REASON
IF WS-COMPCODE = MQCC-OK
DISPLAY 'Message sent successfully'
ELSE
DISPLAY 'MQPUT failed, reason=' WS-REASON
PERFORM 9800-MQ-ERROR
END-IF
* Close the queue
MOVE MQCO-NONE TO WS-OPTIONS
CALL 'MQCLOSE'
USING WS-HCONN
WS-HOBJ
WS-OPTIONS
WS-COMPCODE
WS-REASON.
Getting a Message from a Queue
3100-RECEIVE-MESSAGE.
* -----------------------------------------------
* Open queue for input (shared access)
* -----------------------------------------------
MOVE MQOT-Q TO MQOD-OBJECTTYPE
MOVE 'GBANK.INQUIRY.REQUEST'
TO MQOD-OBJECTNAME
COMPUTE WS-OPTIONS =
MQOO-INPUT-SHARED + MQOO-FAIL-IF-QUIESCING
CALL 'MQOPEN'
USING WS-HCONN
WS-OBJ-DESC
WS-OPTIONS
WS-HOBJ
WS-COMPCODE
WS-REASON
IF WS-COMPCODE NOT = MQCC-OK
DISPLAY 'MQOPEN for input failed, '
'reason=' WS-REASON
PERFORM 9800-MQ-ERROR
END-IF
* -----------------------------------------------
* Get next message (wait up to 30 seconds)
* -----------------------------------------------
MOVE MQGMO-DEFAULT TO WS-GMO
COMPUTE MQGMO-OPTIONS =
MQGMO-WAIT + MQGMO-FAIL-IF-QUIESCING
MOVE 30000 TO MQGMO-WAITINTERVAL
MOVE 4096 TO WS-BUFFER-LENGTH
CALL 'MQGET'
USING WS-HCONN
WS-HOBJ
WS-MSG-DESC
WS-GMO
WS-BUFFER-LENGTH
WS-MSG-BUFFER
WS-DATA-LENGTH
WS-COMPCODE
WS-REASON
EVALUATE TRUE
WHEN WS-COMPCODE = MQCC-OK
DISPLAY 'Message received, length='
WS-DATA-LENGTH
PERFORM 3200-PROCESS-MESSAGE
WHEN WS-REASON = MQRC-NO-MSG-AVAILABLE
DISPLAY 'No message available (timeout)'
WHEN OTHER
DISPLAY 'MQGET failed, reason='
WS-REASON
PERFORM 9800-MQ-ERROR
END-EVALUATE.
⚠️ Critical Point: Always handle the MQRC-NO-MSG-AVAILABLE reason code. In a loop-based message consumer, this is normal behavior when the queue is empty — it is not an error.
Request/Reply Pattern
Many integration scenarios use request/reply: a client sends a request message and waits for a response. The COBOL program acts as the service:
3200-PROCESS-REQUEST-REPLY.
* -----------------------------------------------
* 1. Get request from request queue
* 2. Process the request
* 3. Build response
* 4. Put response on reply queue (from MQMD)
* -----------------------------------------------
* Save the reply-to information from the request
MOVE MQMD-REPLYTOQ OF WS-MSG-DESC
TO WS-REPLY-QUEUE-NAME
MOVE MQMD-REPLYTOQMGR OF WS-MSG-DESC
TO WS-REPLY-QMGR-NAME
MOVE MQMD-MSGID OF WS-MSG-DESC
TO WS-CORRELID
* Process the request (business logic)
PERFORM 3300-EXECUTE-BUSINESS-LOGIC
* Build response message
PERFORM 3400-BUILD-RESPONSE
* Open reply queue
MOVE WS-REPLY-QUEUE-NAME TO MQOD-OBJECTNAME
MOVE MQOO-OUTPUT TO WS-OPTIONS
CALL 'MQOPEN'
USING WS-HCONN
WS-OBJ-DESC
WS-OPTIONS
WS-HOBJ-REPLY
WS-COMPCODE
WS-REASON
* Set correlation ID so client can match response
MOVE MQMD-DEFAULT TO WS-REPLY-MSG-DESC
MOVE WS-CORRELID
TO MQMD-CORRELID OF WS-REPLY-MSG-DESC
MOVE MQMT-REPLY
TO MQMD-MSGTYPE OF WS-REPLY-MSG-DESC
* Put response
CALL 'MQPUT'
USING WS-HCONN
WS-HOBJ-REPLY
WS-REPLY-MSG-DESC
WS-PMO
WS-RESP-LENGTH
WS-RESP-BUFFER
WS-COMPCODE
WS-REASON.
📊 MQ at GlobalBank
GlobalBank uses MQ extensively: - GBANK.TXN.NOTIFY — Real-time transaction notifications to mobile app - GBANK.INQUIRY.REQUEST / GBANK.INQUIRY.REPLY — Balance inquiry from web - GBANK.ALERT.FRAUD — Fraud detection system triggers - GBANK.FEED.EXTERNAL — External data feeds from card networks
Priya Kapoor designed this architecture to decouple the mainframe from the channels (web, mobile, ATM) that consume its services. "The queue is the contract," she says. "As long as the message format is agreed upon, we can change either side independently."
39.3 JSON Processing in COBOL
Enterprise COBOL 6.1 and later versions provide native JSON support through the JSON PARSE and JSON GENERATE statements. These statements translate between JSON text and COBOL data structures without requiring external libraries.
JSON GENERATE: Creating JSON from COBOL Data
01 WS-ACCOUNT-RESPONSE.
05 ACCT-NUMBER PIC X(10)
VALUE '0012345678'.
05 ACCT-NAME PIC X(30)
VALUE 'CHEN, MARIA'.
05 ACCT-TYPE PIC X(03)
VALUE 'CHK'.
05 ACCT-BALANCE PIC S9(11)V99
VALUE +15847.93.
05 ACCT-STATUS PIC X(01)
VALUE 'A'.
05 ACCT-OPEN-DATE PIC 9(08)
VALUE 20180315.
01 WS-JSON-OUTPUT PIC X(2000).
01 WS-JSON-LENGTH PIC 9(08).
PROCEDURE DIVISION.
* -----------------------------------------------
* Generate JSON from COBOL data structure
* -----------------------------------------------
JSON GENERATE WS-JSON-OUTPUT
FROM WS-ACCOUNT-RESPONSE
COUNT IN WS-JSON-LENGTH
ON EXCEPTION
DISPLAY 'JSON GENERATE failed: '
JSON-STATUS
NOT ON EXCEPTION
DISPLAY 'JSON generated, length='
WS-JSON-LENGTH
DISPLAY WS-JSON-OUTPUT(1:WS-JSON-LENGTH)
END-JSON
The output:
{"ACCT-NUMBER":"0012345678","ACCT-NAME":"CHEN, MARIA","ACCT-TYPE":"CHK","ACCT-BALANCE":15847.93,"ACCT-STATUS":"A","ACCT-OPEN-DATE":20180315}
Controlling JSON Field Names
By default, JSON GENERATE uses the COBOL data names (with hyphens) as JSON field names. You can override this with the NAME clause:
JSON GENERATE WS-JSON-OUTPUT
FROM WS-ACCOUNT-RESPONSE
COUNT IN WS-JSON-LENGTH
NAME OF ACCT-NUMBER IS 'accountNumber'
NAME OF ACCT-NAME IS 'accountName'
NAME OF ACCT-TYPE IS 'accountType'
NAME OF ACCT-BALANCE IS 'balance'
NAME OF ACCT-STATUS IS 'status'
NAME OF ACCT-OPEN-DATE IS 'openDate'
END-JSON
Now the output follows camelCase conventions:
{"accountNumber":"0012345678","accountName":"CHEN, MARIA","accountType":"CHK","balance":15847.93,"status":"A","openDate":20180315}
💡 Key Insight: The NAME clause is essential for producing JSON that matches the API contract expected by modern front-end applications. Without it, your JSON field names will have COBOL-style hyphens that JavaScript and other languages find awkward to work with.
JSON PARSE: Reading JSON into COBOL Data
01 WS-JSON-INPUT PIC X(2000).
01 WS-INQUIRY-REQUEST.
05 REQ-ACCOUNT-NO PIC X(10).
05 REQ-REQUEST-TYPE PIC X(10).
05 REQ-INCLUDE-HISTORY PIC X(05).
PROCEDURE DIVISION.
* -----------------------------------------------
* Parse JSON into COBOL data structure
* -----------------------------------------------
* Assume WS-JSON-INPUT contains:
* {"accountNo":"0012345678",
* "requestType":"BALANCE",
* "includeHistory":"true"}
JSON PARSE WS-JSON-INPUT
INTO WS-INQUIRY-REQUEST
WITH DETAIL
NAME OF REQ-ACCOUNT-NO IS 'accountNo'
NAME OF REQ-REQUEST-TYPE IS 'requestType'
NAME OF REQ-INCLUDE-HISTORY
IS 'includeHistory'
ON EXCEPTION
DISPLAY 'JSON PARSE failed: '
JSON-STATUS
MOVE 'Y' TO WS-PARSE-ERROR
NOT ON EXCEPTION
DISPLAY 'Parsed account: '
REQ-ACCOUNT-NO
DISPLAY 'Request type: '
REQ-REQUEST-TYPE
END-JSON
Handling JSON Arrays
JSON arrays map to COBOL tables (OCCURS):
01 WS-TXN-LIST.
05 TXN-COUNT PIC 9(04).
05 TXN-ENTRY OCCURS 100 TIMES
DEPENDING ON TXN-COUNT.
10 TXN-DATE PIC 9(08).
10 TXN-DESC PIC X(30).
10 TXN-AMOUNT PIC S9(09)V99.
10 TXN-TYPE PIC X(02).
* Generate JSON with array
JSON GENERATE WS-JSON-OUTPUT
FROM WS-TXN-LIST
COUNT IN WS-JSON-LENGTH
NAME OF TXN-ENTRY IS 'transactions'
NAME OF TXN-DATE IS 'date'
NAME OF TXN-DESC IS 'description'
NAME OF TXN-AMOUNT IS 'amount'
NAME OF TXN-TYPE IS 'type'
SUPPRESS TXN-COUNT
END-JSON
This produces:
{
"transactions": [
{"date":20260310,"description":"GROCERY STORE","amount":47.83,"type":"DB"},
{"date":20260310,"description":"DIRECT DEPOSIT","amount":3200.00,"type":"CR"}
]
}
✅ Best Practice: Use the SUPPRESS clause to omit the COBOL table count field (DEPENDING ON variable) from the JSON output. API consumers do not need the count — the array length is implicit in JSON.
39.4 XML Processing in COBOL
XML processing was available in COBOL before JSON and remains important for industries that use XML-based standards like EDI, HL7 (healthcare), and SWIFT (banking).
XML GENERATE
01 WS-CLAIM-RESPONSE.
05 CLM-CLAIM-ID PIC X(12)
VALUE '2026031000123'.
05 CLM-STATUS PIC X(10)
VALUE 'APPROVED'.
05 CLM-ALLOWED-AMT PIC S9(07)V99
VALUE +1250.00.
05 CLM-PATIENT-RESP PIC S9(07)V99
VALUE +250.00.
05 CLM-PLAN-PAYS PIC S9(07)V99
VALUE +1000.00.
01 WS-XML-OUTPUT PIC X(4000).
01 WS-XML-LENGTH PIC 9(08).
XML GENERATE WS-XML-OUTPUT
FROM WS-CLAIM-RESPONSE
COUNT IN WS-XML-LENGTH
WITH XML-DECLARATION
WITH ATTRIBUTES
ON EXCEPTION
DISPLAY 'XML GENERATE failed'
NOT ON EXCEPTION
DISPLAY WS-XML-OUTPUT(1:WS-XML-LENGTH)
END-XML
XML PARSE with Event-Based Processing
XML PARSE uses an event-based model (similar to SAX parsing). You provide a processing procedure that the runtime calls for each XML event:
01 WS-XML-INPUT PIC X(4000).
01 WS-XML-EVENT PIC 9(02).
01 WS-XML-TEXT PIC X(1000).
01 WS-XML-TEXT-LENGTH PIC 9(08).
01 WS-PARSED-DATA.
05 WS-CURRENT-ELEMENT PIC X(50).
05 WS-CLAIM-ID-PARSED PIC X(12).
05 WS-STATUS-PARSED PIC X(10).
05 WS-AMOUNT-PARSED PIC X(15).
XML PARSE WS-XML-INPUT
PROCESSING PROCEDURE IS 8000-XML-HANDLER
ON EXCEPTION
DISPLAY 'XML PARSE failed, code='
XML-CODE
END-XML.
8000-XML-HANDLER.
EVALUATE XML-EVENT
WHEN XML-EVENT-START-OF-ELEMENT
MOVE XML-TEXT TO WS-CURRENT-ELEMENT
WHEN XML-EVENT-CONTENT-CHARACTERS
EVALUATE WS-CURRENT-ELEMENT
WHEN 'CLM-CLAIM-ID'
MOVE XML-TEXT
TO WS-CLAIM-ID-PARSED
WHEN 'CLM-STATUS'
MOVE XML-TEXT
TO WS-STATUS-PARSED
WHEN 'CLM-ALLOWED-AMT'
MOVE XML-TEXT
TO WS-AMOUNT-PARSED
END-EVALUATE
WHEN XML-EVENT-END-OF-ELEMENT
MOVE SPACES TO WS-CURRENT-ELEMENT
WHEN XML-EVENT-END-OF-DOCUMENT
DISPLAY 'XML parsing complete'
END-EVALUATE.
🔗 Cross-Reference: The event-based XML parsing model is similar to the SAX parsers used in Java and Python. If you have used those, the COBOL approach will feel familiar — the main difference is that COBOL uses an EVALUATE block instead of callback methods.
39.5 REST APIs from COBOL
COBOL can both consume (call) and provide (serve) REST APIs. The approach depends on the runtime environment.
COBOL as an API Provider (via CICS)
The most common pattern is exposing COBOL programs as REST APIs through CICS. IBM's z/OS Connect EE (now part of IBM z/OS Connect) maps COBOL programs to RESTful endpoints:
HTTP GET /api/accounts/0012345678
→ CICS routes to COBOL program ACCTINQ
→ ACCTINQ reads the VSAM/DB2 account record
→ ACCTINQ returns data in a COMMAREA
→ z/OS Connect transforms COMMAREA to JSON
→ JSON response sent to HTTP client
The COBOL program itself does not need to know it is being called via REST. It receives its input through a COMMAREA (or container) and returns output the same way. The JSON/HTTP translation happens in the infrastructure layer.
IDENTIFICATION DIVISION.
PROGRAM-ID. ACCTINQ.
*
* Account Inquiry - called via CICS COMMAREA
* Can be invoked from 3270 screen, MQ, or REST API
*
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-RESPONSE-FIELDS.
05 WS-RESP PIC S9(08) COMP.
05 WS-RESP2 PIC S9(08) COMP.
LINKAGE SECTION.
01 DFHCOMMAREA.
05 COMM-REQUEST.
10 COMM-FUNCTION PIC X(08).
10 COMM-ACCT-NO PIC X(10).
05 COMM-RESPONSE.
10 COMM-RETURN-CODE PIC X(02).
10 COMM-ACCT-NAME PIC X(30).
10 COMM-ACCT-TYPE PIC X(03).
10 COMM-BALANCE PIC S9(11)V99 COMP-3.
10 COMM-STATUS PIC X(01).
10 COMM-OPEN-DATE PIC 9(08).
10 COMM-ERROR-MSG PIC X(50).
PROCEDURE DIVISION.
0000-MAIN.
EVALUATE COMM-FUNCTION
WHEN 'INQUIRY '
PERFORM 1000-BALANCE-INQUIRY
WHEN 'HISTORY '
PERFORM 2000-TXN-HISTORY
WHEN OTHER
MOVE '99' TO COMM-RETURN-CODE
MOVE 'Invalid function'
TO COMM-ERROR-MSG
END-EVALUATE
EXEC CICS RETURN END-EXEC.
1000-BALANCE-INQUIRY.
* Read account from DB2
EXEC SQL
SELECT ACCT_NAME, ACCT_TYPE,
CURRENT_BALANCE, ACCT_STATUS,
OPEN_DATE
INTO :COMM-ACCT-NAME,
:COMM-ACCT-TYPE,
:COMM-BALANCE,
:COMM-STATUS,
:COMM-OPEN-DATE
FROM ACCOUNT_MASTER
WHERE ACCOUNT_NO = :COMM-ACCT-NO
END-EXEC
EVALUATE SQLCODE
WHEN 0
MOVE '00' TO COMM-RETURN-CODE
WHEN +100
MOVE '01' TO COMM-RETURN-CODE
MOVE 'Account not found'
TO COMM-ERROR-MSG
WHEN OTHER
MOVE '99' TO COMM-RETURN-CODE
STRING 'DB2 error: SQLCODE='
SQLCODE
DELIMITED SIZE
INTO COMM-ERROR-MSG
END-EVALUATE.
📊 z/OS Connect Mapping
The z/OS Connect service definition maps the COMMAREA to REST:
# z/OS Connect service definition (simplified)
service:
name: accountInquiry
path: /api/accounts/{accountNumber}
method: GET
program: ACCTINQ
inputMapping:
COMM-FUNCTION: "INQUIRY"
COMM-ACCT-NO: "{accountNumber}"
outputMapping:
200:
accountName: COMM-ACCT-NAME
accountType: COMM-ACCT-TYPE
balance: COMM-BALANCE
status: COMM-STATUS
404:
condition: COMM-RETURN-CODE = "01"
error: COMM-ERROR-MSG
COBOL as an API Consumer
COBOL can also call external REST APIs. On z/OS, this is done through the HTTP client facilities of CICS Web Services or through the z/OS Client Web Enablement Toolkit:
1000-CALL-EXTERNAL-API.
* -----------------------------------------------
* Use CICS WEB SEND/RECEIVE to call REST API
* -----------------------------------------------
MOVE '/api/v2/exchange-rates/USD'
TO WS-URL-PATH
* Set up the HTTP request
EXEC CICS WEB OPEN
HOST(WS-API-HOST)
HOSTLENGTH(LENGTH OF WS-API-HOST)
PORTNUMBER(WS-API-PORT)
SCHEME(HTTPS)
SESSTOKEN(WS-SESSION-TOKEN)
RESP(WS-RESP)
RESP2(WS-RESP2)
END-EXEC
* Send HTTP GET request
EXEC CICS WEB SEND
SESSTOKEN(WS-SESSION-TOKEN)
GET
PATH(WS-URL-PATH)
PATHLENGTH(LENGTH OF WS-URL-PATH)
MEDIATYPE('application/json')
RESP(WS-RESP)
RESP2(WS-RESP2)
END-EXEC
* Receive the response
EXEC CICS WEB RECEIVE
SESSTOKEN(WS-SESSION-TOKEN)
INTO(WS-RESPONSE-BUFFER)
LENGTH(WS-RESPONSE-LENGTH)
STATUSCODE(WS-HTTP-STATUS)
RESP(WS-RESP)
RESP2(WS-RESP2)
END-EXEC
IF WS-HTTP-STATUS = 200
PERFORM 1100-PARSE-EXCHANGE-RATE
ELSE
DISPLAY 'API call failed, HTTP status='
WS-HTTP-STATUS
END-IF
* Close the connection
EXEC CICS WEB CLOSE
SESSTOKEN(WS-SESSION-TOKEN)
RESP(WS-RESP)
END-EXEC.
1100-PARSE-EXCHANGE-RATE.
* Parse JSON response
JSON PARSE WS-RESPONSE-BUFFER
INTO WS-EXCHANGE-RATE-DATA
NAME OF ER-CURRENCY IS 'currency'
NAME OF ER-RATE IS 'rate'
NAME OF ER-TIMESTAMP IS 'timestamp'
END-JSON.
39.6 Event-Driven Patterns
Event-driven architecture (EDA) enables systems to react to events as they happen, rather than polling for changes. COBOL systems participate in EDA primarily through MQ.
The Message Listener Pattern
A COBOL message listener continuously reads from a queue and processes messages as they arrive:
0000-MAIN-CONTROL.
PERFORM 1000-INITIALIZATION
PERFORM 2000-MESSAGE-LOOP
UNTIL WS-SHUTDOWN-REQUESTED
PERFORM 9000-TERMINATION
STOP RUN.
2000-MESSAGE-LOOP.
* -----------------------------------------------
* Wait for next message with timeout
* Process it, then loop
* -----------------------------------------------
MOVE MQGMO-DEFAULT TO WS-GMO
COMPUTE MQGMO-OPTIONS =
MQGMO-WAIT +
MQGMO-FAIL-IF-QUIESCING +
MQGMO-SYNCPOINT
MOVE 5000 TO MQGMO-WAITINTERVAL
CALL 'MQGET'
USING WS-HCONN
WS-HOBJ
WS-MSG-DESC
WS-GMO
WS-BUFFER-LENGTH
WS-MSG-BUFFER
WS-DATA-LENGTH
WS-COMPCODE
WS-REASON
EVALUATE TRUE
WHEN WS-COMPCODE = MQCC-OK
ADD 1 TO WS-MSGS-PROCESSED
PERFORM 2100-ROUTE-MESSAGE
WHEN WS-REASON = MQRC-NO-MSG-AVAILABLE
* Timeout - no message, check for shutdown
PERFORM 2900-CHECK-SHUTDOWN
WHEN WS-REASON =
MQRC-CONNECTION-QUIESCING
DISPLAY 'Queue manager quiescing'
MOVE 'Y' TO WS-SHUTDOWN-REQUESTED
WHEN OTHER
DISPLAY 'MQGET error, reason='
WS-REASON
PERFORM 9800-MQ-ERROR
END-EVALUATE.
2100-ROUTE-MESSAGE.
* Route based on message content type
EVALUATE WS-MSG-TYPE-HEADER
WHEN 'TXN-NOTIFY'
PERFORM 3000-PROCESS-TXN-NOTIFICATION
WHEN 'BAL-INQUIRY'
PERFORM 4000-PROCESS-BAL-INQUIRY
WHEN 'FRAUD-ALERT'
PERFORM 5000-PROCESS-FRAUD-ALERT
WHEN OTHER
PERFORM 6000-HANDLE-UNKNOWN-MSG
END-EVALUATE.
The Publish/Subscribe Pattern
In publish/subscribe, a message is sent to a topic, and all subscribers to that topic receive a copy. MQ supports this through topic objects:
* Subscribe to a topic
MOVE MQSD-DEFAULT TO WS-SUB-DESC
MOVE 'GBANK/TRANSACTIONS/LARGE'
TO MQSD-OBJECTSTRING OF WS-SUB-DESC
COMPUTE MQSD-OPTIONS OF WS-SUB-DESC =
MQSO-CREATE + MQSO-MANAGED +
MQSO-NON-DURABLE
CALL 'MQSUB'
USING WS-HCONN
WS-SUB-DESC
WS-HOBJ
WS-HSUB
WS-COMPCODE
WS-REASON
This enables scenarios like: every transaction over $10,000 is published to a topic, and the fraud detection system, the compliance system, and the management dashboard all receive the notification independently.
39.7 Data Transformation Patterns
A central challenge in integration is transforming data between COBOL's fixed-format world and the modern world's flexible formats.
The Transformation Layer Pattern
* -----------------------------------------------
* Transform COBOL record to JSON-friendly format
* Handle: COMP-3 to display, EBCDIC to ASCII,
* packed dates to ISO format
* -----------------------------------------------
5000-TRANSFORM-TO-API.
* Convert packed decimal balance to display
MOVE ACCT-BALANCE TO WS-BALANCE-DISPLAY
* Convert COBOL date (YYYYMMDD) to ISO 8601
STRING ACCT-OPEN-DATE(1:4) '-'
ACCT-OPEN-DATE(5:2) '-'
ACCT-OPEN-DATE(7:2)
DELIMITED SIZE
INTO WS-ISO-DATE
* Trim trailing spaces from name
MOVE FUNCTION TRIM(ACCT-NAME TRAILING)
TO WS-TRIMMED-NAME
* Build the API-ready structure
MOVE ACCT-ACCOUNT-NO TO API-ACCOUNT-NUMBER
MOVE WS-TRIMMED-NAME TO API-ACCOUNT-NAME
MOVE WS-BALANCE-DISPLAY
TO API-BALANCE
MOVE WS-ISO-DATE TO API-OPEN-DATE
* Now generate JSON from the API structure
JSON GENERATE WS-JSON-OUTPUT
FROM WS-API-RESPONSE
COUNT IN WS-JSON-LENGTH
END-JSON.
Handling Character Encoding
On z/OS, COBOL data is in EBCDIC. REST APIs expect UTF-8 (or at minimum, ASCII). The translation happens at different levels:
- CICS Web Services handles encoding conversion automatically
- MQ can convert encoding using data conversion exits
- z/OS Connect maps between EBCDIC and UTF-8 in the service definition
- Programmatic conversion is possible using Language Environment conversion services
* Convert EBCDIC string to UTF-8
* (using LE conversion services)
CALL 'CEEUCNV'
USING WS-EBCDIC-STRING
WS-UTF8-BUFFER
WS-INPUT-CCSID (value 1047 EBCDIC)
WS-OUTPUT-CCSID (value 1208 UTF-8)
WS-FEEDBACK-CODE
⚠️ Watch Out: Numeric fields in COMP-3 (packed decimal) format cannot be sent directly in JSON — they are binary representations that look like garbage when interpreted as text. Always convert COMP-3 to DISPLAY format before including in JSON or XML output.
39.8 GlobalBank: Real-Time Balance Inquiry API
Let us walk through a complete real-time integration scenario at GlobalBank.
The Business Requirement
GlobalBank's mobile app needs a real-time balance inquiry API. When a customer opens the app, it calls:
GET /api/v1/accounts/{accountNumber}/balance
Authorization: Bearer <JWT token>
The response must return within 200 milliseconds.
The Architecture
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ Mobile App │───▶│ API Gateway │───▶│ z/OS Connect│
│ (iOS/Android)│ │ (Kong/Apigee)│ │ │
└──────────────┘ └───────────────┘ └──────┬───────┘
│
┌──────▼───────┐
│ CICS Region │
│ ACCTINQ pgm │
└──────┬───────┘
│
┌──────▼───────┐
│ DB2 │
│ ACCOUNT_MASTER│
└──────────────┘
Response Time Breakdown
| Component | Time |
|---|---|
| Mobile to API Gateway | 40-80ms (network) |
| API Gateway auth/routing | 5-10ms |
| z/OS Connect transformation | 2-5ms |
| CICS dispatch | 1-2ms |
| COBOL program execution | 3-8ms |
| DB2 query | 2-5ms |
| Return path | 10-20ms |
| Total | 63-130ms |
The COBOL program is the fastest component in the chain. Maria Chen's ACCTINQ program processes a balance inquiry in under 10 milliseconds. "People assume the mainframe is the bottleneck," she says. "It is almost always the network."
MQ-Based Transaction Notifications
When a transaction posts to an account, GlobalBank sends a real-time notification through MQ:
4500-SEND-TXN-NOTIFICATION.
* -----------------------------------------------
* After posting a transaction, notify
* the real-time channel via MQ
* -----------------------------------------------
IF TXN-AMOUNT > 100.00
OR ACCT-NOTIFICATION-PREF = 'ALL'
* Build notification JSON
INITIALIZE WS-NOTIFY-BUFFER
STRING
'{"event":"TRANSACTION_POSTED",'
'"accountNo":"' ACCT-ACCOUNT-NO '",'
'"amount":' WS-TXN-AMT-DISP ','
'"balance":' WS-NEW-BAL-DISP ','
'"description":"'
WS-TXN-DESCRIPTION '",'
'"timestamp":"'
WS-ISO-TIMESTAMP '"}'
DELIMITED SIZE
INTO WS-NOTIFY-BUFFER
POINTER WS-NOTIFY-LENGTH
SUBTRACT 1 FROM WS-NOTIFY-LENGTH
PERFORM 4510-MQPUT-NOTIFICATION
END-IF.
The mobile app has a persistent connection to a notification service that reads from the MQ queue and pushes the event to the device. The customer sees a notification within seconds of the transaction posting.
39.9 MedClaim: Real-Time Eligibility Check
At MedClaim, the real-time integration challenge is different: healthcare providers need instant eligibility verification at the point of service.
The Scenario
A patient presents their insurance card at a doctor's office. The office management system calls MedClaim's eligibility API:
POST /api/v1/eligibility/check
Content-Type: application/json
{
"memberId": "MED8847291",
"providerId": "NPI1234567890",
"serviceDate": "2026-03-11",
"serviceCode": "99213",
"diagnosisCode": "J06.9"
}
The COBOL Eligibility Program
IDENTIFICATION DIVISION.
PROGRAM-ID. ELIGCHK.
*
* Real-time eligibility check
* Called via CICS COMMAREA from z/OS Connect
*
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-DB2-FIELDS.
05 WS-PLAN-TYPE PIC X(03).
05 WS-COVERAGE-EFF PIC X(10).
05 WS-COVERAGE-TERM PIC X(10).
05 WS-DEDUCTIBLE-MAX PIC S9(07)V99 COMP-3.
05 WS-DEDUCTIBLE-MET PIC S9(07)V99 COMP-3.
05 WS-COPAY-AMOUNT PIC S9(05)V99 COMP-3.
05 WS-OOP-MAX PIC S9(07)V99 COMP-3.
05 WS-OOP-MET PIC S9(07)V99 COMP-3.
LINKAGE SECTION.
01 DFHCOMMAREA.
05 COMM-REQUEST.
10 COMM-MEMBER-ID PIC X(10).
10 COMM-PROVIDER-ID PIC X(13).
10 COMM-SERVICE-DATE PIC X(10).
10 COMM-SERVICE-CODE PIC X(05).
10 COMM-DIAGNOSIS PIC X(07).
05 COMM-RESPONSE.
10 COMM-ELIGIBLE PIC X(01).
88 COMM-IS-ELIGIBLE VALUE 'Y'.
88 COMM-NOT-ELIGIBLE VALUE 'N'.
10 COMM-PLAN-TYPE PIC X(03).
10 COMM-COPAY PIC S9(05)V99 COMP-3.
10 COMM-DEDUCT-REMAINING
PIC S9(07)V99 COMP-3.
10 COMM-OOP-REMAINING PIC S9(07)V99 COMP-3.
10 COMM-COVERAGE-PCT PIC 9(03).
10 COMM-STATUS-CODE PIC X(02).
10 COMM-STATUS-MSG PIC X(50).
PROCEDURE DIVISION.
0000-MAIN.
PERFORM 1000-CHECK-MEMBER
IF COMM-IS-ELIGIBLE
PERFORM 2000-CHECK-COVERAGE
PERFORM 3000-CHECK-PROVIDER
PERFORM 4000-CALCULATE-PATIENT-COST
END-IF
EXEC CICS RETURN END-EXEC.
1000-CHECK-MEMBER.
* Verify member is active
EXEC SQL
SELECT PLAN_TYPE,
COVERAGE_EFFECTIVE,
COVERAGE_TERMINATION,
ANNUAL_DEDUCTIBLE,
DEDUCTIBLE_YTD,
OOP_MAXIMUM,
OOP_YTD
INTO :WS-PLAN-TYPE,
:WS-COVERAGE-EFF,
:WS-COVERAGE-TERM,
:WS-DEDUCTIBLE-MAX,
:WS-DEDUCTIBLE-MET,
:WS-OOP-MAX,
:WS-OOP-MET
FROM MEMBER_COVERAGE
WHERE MEMBER_ID = :COMM-MEMBER-ID
AND COVERAGE_EFFECTIVE <=
:COMM-SERVICE-DATE
AND COVERAGE_TERMINATION >=
:COMM-SERVICE-DATE
END-EXEC
EVALUATE SQLCODE
WHEN 0
SET COMM-IS-ELIGIBLE TO TRUE
MOVE WS-PLAN-TYPE TO COMM-PLAN-TYPE
MOVE '00' TO COMM-STATUS-CODE
WHEN +100
SET COMM-NOT-ELIGIBLE TO TRUE
MOVE '01' TO COMM-STATUS-CODE
MOVE 'Member not found or not active'
TO COMM-STATUS-MSG
WHEN OTHER
SET COMM-NOT-ELIGIBLE TO TRUE
MOVE '99' TO COMM-STATUS-CODE
MOVE 'System error during lookup'
TO COMM-STATUS-MSG
END-EVALUATE.
The Response
The API returns:
{
"eligible": true,
"planType": "PPO",
"copay": 40.00,
"deductibleRemaining": 750.00,
"outOfPocketRemaining": 3250.00,
"coveragePercentage": 80,
"statusCode": "00",
"statusMessage": ""
}
The doctor's office sees this within 300 milliseconds of swiping the insurance card.
🔵 MedClaim Architecture Note: James Okafor's team handles approximately 50,000 eligibility checks per day through this API. The COBOL program (ELIGCHK) has been in production for seven years with zero logic defects. "It is the most boring, reliable piece of code in our system," James says. "Which is exactly what you want for something that determines whether a patient gets care."
39.10 API Gateway Integration
An API gateway sits between external consumers and your COBOL services, providing:
- Authentication and authorization (OAuth 2.0, API keys)
- Rate limiting (prevent abuse)
- Request/response transformation (format conversion)
- Monitoring and analytics (track API usage)
- Versioning (support multiple API versions)
COBOL Behind the Gateway
Your COBOL program does not interact with the API gateway directly. The layered architecture looks like:
External Client
↓ HTTPS + JWT
API Gateway (Kong / Apigee / AWS API Gateway)
↓ HTTP
z/OS Connect EE
↓ COMMAREA / Channel/Container
CICS + COBOL Program
↓ SQL
DB2
The COBOL program receives clean, validated input through its COMMAREA. All the security, rate limiting, and format transformation happens upstream.
Versioning Strategy
When you need to change an API, the COBOL program can handle multiple versions through the COMMAREA:
LINKAGE SECTION.
01 DFHCOMMAREA.
05 COMM-API-VERSION PIC X(02).
05 COMM-DATA PIC X(998).
01 COMM-V1-REQUEST REDEFINES DFHCOMMAREA.
05 FILLER PIC X(02).
05 CV1-ACCOUNT-NO PIC X(10).
05 CV1-FUNCTION PIC X(08).
01 COMM-V2-REQUEST REDEFINES DFHCOMMAREA.
05 FILLER PIC X(02).
05 CV2-ACCOUNT-NO PIC X(10).
05 CV2-FUNCTION PIC X(08).
05 CV2-INCLUDE-HISTORY PIC X(01).
05 CV2-HISTORY-DAYS PIC 9(03).
PROCEDURE DIVISION.
EVALUATE COMM-API-VERSION
WHEN '01'
PERFORM 1000-PROCESS-V1
WHEN '02'
PERFORM 2000-PROCESS-V2
WHEN OTHER
MOVE 'Unsupported API version'
TO COMM-ERROR-MSG
END-EVALUATE.
39.11 Error Handling in Integration
Integration introduces failure modes that do not exist in standalone programs. Networks fail, remote systems go down, messages get lost, and timeouts expire. Robust error handling is essential.
The Dead Letter Queue Pattern
When a message cannot be processed, do not discard it — move it to a dead letter queue (DLQ) for later investigation:
7000-HANDLE-POISON-MESSAGE.
* -----------------------------------------------
* Message failed processing 3 times
* Move to dead letter queue for investigation
* -----------------------------------------------
MOVE 'GBANK.DLQ' TO MQOD-OBJECTNAME
MOVE MQOO-OUTPUT TO WS-OPTIONS
CALL 'MQOPEN'
USING WS-HCONN
WS-DLQ-OBJ-DESC
WS-OPTIONS
WS-HOBJ-DLQ
WS-COMPCODE
WS-REASON
IF WS-COMPCODE = MQCC-OK
* Add error information to message header
MOVE 'PROCESSING_FAILED'
TO WS-DLQ-REASON
MOVE WS-RETRY-COUNT
TO WS-DLQ-RETRY-COUNT
MOVE WS-ERROR-DETAILS
TO WS-DLQ-ERROR-INFO
CALL 'MQPUT'
USING WS-HCONN
WS-HOBJ-DLQ
WS-MSG-DESC
WS-PMO
WS-BUFFER-LENGTH
WS-MSG-BUFFER
WS-COMPCODE
WS-REASON
END-IF.
Retry Logic
6000-CALL-WITH-RETRY.
MOVE 0 TO WS-RETRY-COUNT
MOVE 'N' TO WS-CALL-SUCCESSFUL
PERFORM UNTIL WS-CALL-SUCCESSFUL = 'Y'
OR WS-RETRY-COUNT >= 3
PERFORM 6100-MAKE-API-CALL
IF WS-HTTP-STATUS = 200
MOVE 'Y' TO WS-CALL-SUCCESSFUL
ELSE IF WS-HTTP-STATUS = 503
* Service unavailable - retry with backoff
ADD 1 TO WS-RETRY-COUNT
IF WS-RETRY-COUNT < 3
DISPLAY 'Retry ' WS-RETRY-COUNT
' after backoff'
* Note: In CICS, use DELAY instead
EXEC CICS DELAY
FOR SECONDS(WS-RETRY-COUNT * 2)
END-EXEC
END-IF
ELSE
* Non-retryable error
MOVE 3 TO WS-RETRY-COUNT
END-IF
END-PERFORM.
⚖️ Theme — The Modernization Spectrum: Real-time integration represents a middle ground on the modernization spectrum. You do not need to rewrite the COBOL business logic — you just need to make it accessible through modern interfaces. The programs Maria and James wrote years ago still run the same logic. What changed is how the outside world communicates with them.
39.12 Try It Yourself: Building an MQ Message Processor
Student Mainframe Lab Exercise
If you have access to IBM MQ (through IBM Z Xplore or a lab installation), build a COBOL program that:
- Connects to the queue manager
- Opens a queue for input
- Reads messages in a loop (with a 10-second timeout)
- Parses each message (assume simple key=value format)
- Processes the message (look up a record in a file)
- Builds a response message
- Puts the response on a reply queue
GnuCOBOL Alternative
If you do not have access to MQ, simulate the pattern using files: 1. Read "messages" from a request file (one JSON object per line) 2. Parse each JSON object using string manipulation (GnuCOBOL may not have JSON PARSE) 3. Process the request 4. Write a response to a response file
The logic structure is the same — only the I/O mechanism differs.
39.13 Performance Considerations for Integration
Connection Pooling
Opening an MQ connection or HTTP connection for every request is expensive. In CICS, connections are pooled automatically. In batch, open the connection once during initialization and reuse it:
1000-INITIALIZATION.
PERFORM 1100-CONNECT-TO-MQ
PERFORM 1200-OPEN-QUEUES
* Connection and queues remain open for all messages
PERFORM 2000-MESSAGE-LOOP
UNTIL WS-SHUTDOWN
PERFORM 9000-CLOSE-AND-DISCONNECT.
Message Size
Keep messages small. A balance inquiry response of 200 bytes travels much faster than a 50KB transaction history dump. For large payloads, consider: - Returning a reference (URL) to the full data instead of the data itself - Paginating results - Compressing the payload
Asynchronous vs. Synchronous
Not everything needs to be synchronous. Transaction notifications can be fire-and-forget (asynchronous MQPUT with no reply queue). Balance inquiries need a synchronous response. Choose the pattern that matches the business requirement.
39.14 MQ Message Formats and Headers in Depth
Understanding MQ message internals is essential for debugging integration problems. When a message arrives and your program cannot process it, the first thing you examine is the message descriptor (MQMD) and the message content format.
The MQMD Structure
The Message Descriptor is a 364-byte structure that accompanies every MQ message. The fields most relevant to COBOL developers are:
* Key MQMD fields for integration work
01 WS-MSG-DESC-DETAIL.
* Message format — tells receiver how to interpret body
05 MQMD-FORMAT PIC X(08).
* MQFMT-STRING = plain text (JSON, XML)
* MQFMT-NONE = binary (COBOL record as-is)
* MQFMT-CICS = CICS bridge format
* Encoding — byte order and numeric format
05 MQMD-ENCODING PIC S9(09) BINARY.
* Character set — EBCDIC (500) or UTF-8 (1208)
05 MQMD-CCSID PIC S9(09) BINARY.
* Message type
05 MQMD-MSGTYPE PIC S9(09) BINARY.
* MQMT-DATAGRAM = fire and forget
* MQMT-REQUEST = expects a reply
* MQMT-REPLY = response to a request
* Persistence
05 MQMD-PERSISTENCE PIC S9(09) BINARY.
* MQPER-PERSISTENT = survives queue mgr restart
* MQPER-NOT-PERSISTENT = lost on restart
* Expiry (in tenths of a second)
05 MQMD-EXPIRY PIC S9(09) BINARY.
* Correlation ID (for matching requests to replies)
05 MQMD-CORRELID PIC X(24).
* Reply-to queue name
05 MQMD-REPLYTOQ PIC X(48).
* Reply-to queue manager
05 MQMD-REPLYTOQMGR PIC X(48).
Common Message Format Patterns
In practice, three message body formats dominate COBOL integration:
Pattern 1: JSON String Messages
The most common modern pattern. The message body is a UTF-8 JSON string. MQMD-FORMAT is set to MQFMT-STRING and MQMD-CCSID to 1208 (UTF-8). The COBOL program uses JSON PARSE to extract fields.
* Receiving a JSON message
IF MQMD-FORMAT = MQFMT-STRING
IF MQMD-CCSID = 1208
PERFORM 3100-PARSE-JSON-MESSAGE
ELSE IF MQMD-CCSID = 500
* EBCDIC string — may need conversion
PERFORM 3150-CONVERT-AND-PARSE
END-IF
END-IF
Pattern 2: Fixed-Format COBOL Records
For COBOL-to-COBOL communication (common in mainframe-to-mainframe integration), the message body is a raw COBOL record — a copybook layout transmitted as binary. MQMD-FORMAT is MQFMT-NONE. The receiver simply moves the buffer into a matching working-storage structure.
* Receiving a fixed-format record
IF MQMD-FORMAT = MQFMT-NONE
MOVE WS-MSG-BUFFER(1:WS-DATA-LENGTH)
TO WS-INCOMING-TXN-RECORD
* No parsing needed — direct field access
PERFORM 3200-PROCESS-TXN-RECORD
END-IF
Pattern 3: XML Messages
Used in healthcare (HL7/FHIR), banking (SWIFT/ISO 20022), and government (NIEM). MQMD-FORMAT is MQFMT-STRING. The COBOL program uses XML PARSE.
📊 MQ Message Format Decision Matrix
| Scenario | Format | MQMD-FORMAT | CCSID | Why |
|---|---|---|---|---|
| Mobile/Web API response | JSON | MQFMT-STRING | 1208 | Industry standard for APIs |
| Mainframe-to-mainframe | COBOL record | MQFMT-NONE | N/A | No conversion overhead |
| Healthcare integration | XML (HL7) | MQFMT-STRING | 1208 | Industry standard for health |
| Financial messaging | XML (ISO 20022) | MQFMT-STRING | 1208 | SWIFT/payment standard |
| Legacy interface | Fixed text | MQFMT-STRING | 500 | Backward compatibility |
Debugging MQ Message Issues
When a message arrives but your program cannot process it, use this diagnostic sequence:
9700-DIAGNOSE-MESSAGE.
DISPLAY '=== MQ MESSAGE DIAGNOSTIC ==='
DISPLAY 'FORMAT: [' MQMD-FORMAT ']'
DISPLAY 'CCSID: ' MQMD-CCSID
DISPLAY 'ENCODING: ' MQMD-ENCODING
DISPLAY 'MSGTYPE: ' MQMD-MSGTYPE
DISPLAY 'LENGTH: ' WS-DATA-LENGTH
DISPLAY 'FIRST 80 BYTES:'
DISPLAY WS-MSG-BUFFER(1:80)
DISPLAY '=== END DIAGNOSTIC ==='.
⚠️ Common Debugging Trap: If you see garbled characters in the message body, the most likely cause is a CCSID mismatch. The sender wrote the message in UTF-8 (CCSID 1208) but your program is reading it as EBCDIC (CCSID 500), or vice versa. Check the MQMD-CCSID field first.
39.15 JSON PARSE Error Handling in Detail
JSON PARSE can fail for many reasons: malformed JSON, missing fields, type mismatches, buffer overflow. Robust error handling is essential for production integration.
The JSON-STATUS Special Register
When JSON PARSE raises an exception, the special register JSON-STATUS contains a numeric code indicating the error:
5000-SAFE-JSON-PARSE.
JSON PARSE WS-JSON-INPUT
INTO WS-REQUEST-DATA
WITH DETAIL
NAME OF REQ-ACCT-NO IS 'accountNo'
NAME OF REQ-TXN-TYPE IS 'txnType'
NAME OF REQ-AMOUNT IS 'amount'
ON EXCEPTION
PERFORM 5100-HANDLE-JSON-ERROR
NOT ON EXCEPTION
PERFORM 5200-PROCESS-PARSED-DATA
END-JSON.
5100-HANDLE-JSON-ERROR.
EVALUATE JSON-STATUS
WHEN 1
MOVE 'JSON syntax error'
TO WS-ERROR-MSG
WHEN 2
MOVE 'JSON name not matched to COBOL'
TO WS-ERROR-MSG
WHEN 3
MOVE 'JSON value too long for field'
TO WS-ERROR-MSG
WHEN 4
MOVE 'JSON number conversion error'
TO WS-ERROR-MSG
WHEN OTHER
STRING 'JSON PARSE error code: '
JSON-STATUS
DELIMITED SIZE
INTO WS-ERROR-MSG
END-EVALUATE
DISPLAY 'JSON PARSE FAILED: ' WS-ERROR-MSG
DISPLAY 'INPUT WAS: '
WS-JSON-INPUT(1:200)
ADD 1 TO WS-JSON-ERROR-COUNT.
Defensive JSON Parsing Patterns
In production, you cannot assume incoming JSON is well-formed. Defensive parsing includes:
Validating before parsing:
5050-VALIDATE-JSON-BASIC.
* Quick sanity checks before parsing
IF WS-DATA-LENGTH < 2
MOVE 'Message too short for JSON'
TO WS-ERROR-MSG
MOVE 'Y' TO WS-PARSE-ERROR
ELSE IF WS-MSG-BUFFER(1:1) NOT = '{'
AND WS-MSG-BUFFER(1:1) NOT = '['
MOVE 'Message does not start with { or ['
TO WS-ERROR-MSG
MOVE 'Y' TO WS-PARSE-ERROR
END-IF.
Handling missing optional fields:
* Initialize all fields to defaults before parsing
* Missing JSON fields will retain their defaults
INITIALIZE WS-REQUEST-DATA
MOVE SPACES TO REQ-OPTIONAL-NOTE
MOVE 0 TO REQ-OPTIONAL-LIMIT
MOVE 'N' TO REQ-INCLUDE-HISTORY
* Parse — missing fields remain at defaults
JSON PARSE WS-JSON-INPUT
INTO WS-REQUEST-DATA
WITH DETAIL
NAME OF ...
ON EXCEPTION
PERFORM 5100-HANDLE-JSON-ERROR
END-JSON
* Validate required fields
IF REQ-ACCT-NO = SPACES
MOVE 'Missing required field: accountNo'
TO WS-ERROR-MSG
MOVE 'Y' TO WS-PARSE-ERROR
END-IF.
Logging malformed messages for analysis:
5300-LOG-BAD-MESSAGE.
* Write the bad message to an error file for analysis
MOVE WS-MSG-BUFFER TO ERR-MESSAGE-BODY
MOVE WS-ERROR-MSG TO ERR-ERROR-DESCRIPTION
MOVE FUNCTION CURRENT-DATE TO ERR-TIMESTAMP
MOVE MQMD-CORRELID TO ERR-CORRELATION-ID
WRITE ERROR-LOG-RECORD FROM WS-ERROR-LOG-ENTRY
ADD 1 TO WS-MESSAGES-LOGGED.
💡 Key Insight: In a production integration environment, approximately 1-3% of incoming JSON messages will have format issues — missing fields, extra fields, wrong data types, or encoding problems. Your error handling must be as robust as your happy-path processing. At GlobalBank, Priya Kapoor's team found that 80% of integration incidents were caused by upstream systems sending malformed JSON, not by bugs in the COBOL programs.
39.16 XML GENERATE Patterns
While JSON dominates new API development, XML remains critical in regulated industries. COBOL's XML GENERATE is more flexible than many developers realize.
Generating XML with Namespace Support
For standards-compliant XML (SWIFT, HL7, NIEM), you often need XML namespaces:
01 WS-PAYMENT-MSG.
05 PMT-MSG-ID PIC X(20)
VALUE 'MSG2026031100001'.
05 PMT-CREATION-DT PIC X(10)
VALUE '2026-03-11'.
05 PMT-AMOUNT PIC S9(11)V99
VALUE +25000.00.
05 PMT-CURRENCY PIC X(03)
VALUE 'USD'.
05 PMT-DEBTOR-NAME PIC X(35)
VALUE 'GLOBALBANK TREASURY'.
05 PMT-CREDITOR-NAME PIC X(35)
VALUE 'MEDCLAIM RECEIVABLES'.
01 WS-XML-OUTPUT PIC X(8000).
01 WS-XML-LENGTH PIC 9(08).
XML GENERATE WS-XML-OUTPUT
FROM WS-PAYMENT-MSG
COUNT IN WS-XML-LENGTH
WITH XML-DECLARATION
WITH ATTRIBUTES
NAMESPACE IS
'urn:iso:std:iso:20022:tech:xsd:pain.001'
NAMESPACE-PREFIX IS 'pain'
ON EXCEPTION
DISPLAY 'XML GENERATE failed: '
XML-CODE
END-XML
Suppressing Empty Fields
API contracts often require omitting fields that have no value rather than sending empty elements:
XML GENERATE WS-XML-OUTPUT
FROM WS-CLAIM-RESPONSE
COUNT IN WS-XML-LENGTH
SUPPRESS WHEN ZERO
SUPPRESS WHEN SPACES
END-XML
This avoids sending <CLM-NOTES></CLM-NOTES> when the notes field is empty.
Building Complex XML Manually
For XML structures that do not map directly to COBOL data layouts (deeply nested elements, mixed content, attributes on specific elements), you may need to build XML using STRING:
6000-BUILD-COMPLEX-XML.
INITIALIZE WS-XML-OUTPUT
STRING
'<?xml version="1.0" encoding="UTF-8"?>'
'<ClaimResponse xmlns="urn:medclaim:v2">'
'<ClaimId>' CLM-CLAIM-ID '</ClaimId>'
'<Status code="' CLM-STATUS-CODE '">'
CLM-STATUS-DESC
'</Status>'
'<Amounts>'
' <Charged currency="USD">'
WS-CHARGED-DISP
'</Charged>'
' <Allowed currency="USD">'
WS-ALLOWED-DISP
'</Allowed>'
' <PatientResponsibility>'
' <Deductible>'
WS-DEDUCT-DISP
'</Deductible>'
' <Copay>'
WS-COPAY-DISP
'</Copay>'
' </PatientResponsibility>'
'</Amounts>'
'</ClaimResponse>'
DELIMITED SIZE
INTO WS-XML-OUTPUT
POINTER WS-XML-LENGTH
SUBTRACT 1 FROM WS-XML-LENGTH.
⚠️ Warning: Manual XML construction is error-prone — missing closing tags, unescaped special characters (&, <, >), and incorrect nesting are common bugs. Use XML GENERATE whenever possible. Reserve manual construction for cases where the COBOL data structure does not match the required XML schema.
39.17 Event Sourcing Concepts for COBOL Developers
Event sourcing is an architectural pattern where state changes are stored as a sequence of events rather than as the current state alone. While COBOL developers have been doing something similar for decades (audit trails, transaction logs), the formal event sourcing pattern adds structure and intentionality.
The Core Idea
Instead of storing the current account balance as the source of truth, you store every transaction that affected the account. The current balance is derived by replaying the transaction history.
Traditional (state-based):
Account 1234: Balance = $15,847.93
Event sourced:
Account 1234:
Event 1: ACCOUNT_OPENED amount=$0.00 (2018-03-15)
Event 2: DEPOSIT amount=$5,000.00 (2018-03-15)
Event 3: WITHDRAWAL amount=$200.00 (2018-03-20)
Event 4: DEPOSIT amount=$3,200.00 (2018-04-01)
... (thousands of events) ...
Event N: INTEREST_CREDIT amount=$12.41 (2026-03-10)
Derived balance: $15,847.93
Why COBOL Developers Should Understand Event Sourcing
Event sourcing is increasingly used in the cloud-side architectures that surround COBOL systems. When GlobalBank's mobile app publishes a "balance viewed" event, or a "payment initiated" event, these events flow through a Kafka topic that the cloud analytics platform consumes. The COBOL system participates by:
- Publishing events after transactions post (via MQ, as shown in section 39.8)
- Consuming events from external systems (new account applications from the web portal)
- Maintaining the system of record while cloud systems maintain event-derived views
* Publishing a domain event after transaction posting
4600-PUBLISH-DOMAIN-EVENT.
INITIALIZE WS-EVENT-MSG
STRING
'{"eventType":"TRANSACTION_POSTED",'
'"eventId":"' WS-EVENT-UUID '",'
'"timestamp":"' WS-ISO-TIMESTAMP '",'
'"aggregateId":"' ACCT-ACCOUNT-NO '",'
'"payload":{'
' "txnType":"' TXN-TYPE '",'
' "amount":' WS-TXN-AMT-DISP ','
' "prevBalance":' WS-PREV-BAL-DISP ','
' "newBalance":' WS-NEW-BAL-DISP ','
' "description":"' TXN-DESCRIPTION '"'
'}}'
DELIMITED SIZE
INTO WS-EVENT-MSG
POINTER WS-EVENT-LENGTH
SUBTRACT 1 FROM WS-EVENT-LENGTH
PERFORM 4610-MQPUT-EVENT.
The Async vs. Sync Trade-Off
Integration patterns fall on a spectrum from fully synchronous to fully asynchronous:
📊 Synchronous vs. Asynchronous Comparison
| Characteristic | Synchronous (REST) | Asynchronous (MQ) |
|---|---|---|
| Latency | Immediate response | Response delayed |
| Coupling | Tight (both must be up) | Loose (queue buffers) |
| Reliability | Fails if target is down | Queue persists messages |
| Throughput | Limited by slowest component | Each side runs at own pace |
| Complexity | Simple request/response | Correlation, ordering, DLQ |
| Use case | Balance inquiry, eligibility check | Notifications, batch feeds, events |
When to choose synchronous: - The caller needs an immediate answer (balance inquiry, eligibility check) - The operation is idempotent (safe to retry) - Both systems have high availability
When to choose asynchronous: - The caller does not need an immediate answer (notifications, reporting) - The target system may be temporarily unavailable - Volume is high and spiky (buffer with queue) - Decoupling is important for maintainability
At GlobalBank, Priya Kapoor uses a rule of thumb: "If the customer is waiting, it is synchronous. If the customer has moved on, it is asynchronous." Balance inquiries are synchronous; transaction notifications are asynchronous; fraud alerts are asynchronous with high priority.
✅ Best Practice: When in doubt, prefer asynchronous. It is always possible to make an asynchronous call appear synchronous (send request, wait for reply on a temp queue), but it is much harder to make a synchronous architecture tolerate failures gracefully.
39.18 Try It Yourself: JSON Round-Trip Exercise
Student Lab Exercise
Write a COBOL program that demonstrates a complete JSON round-trip:
-
Define a COBOL data structure representing a bank transaction: - Transaction ID (PIC X(12)) - Account Number (PIC X(10)) - Transaction Type (PIC X(03) — DEP, WDL, TRF, FEE) - Amount (PIC S9(09)V99 COMP-3) - Description (PIC X(40)) - Timestamp (PIC X(26))
-
Populate the structure with sample data
-
Generate JSON from the structure using JSON GENERATE with NAME clauses for camelCase field names
-
Display the JSON output to SYSOUT
-
Clear the original structure (INITIALIZE)
-
Parse the JSON back into the same structure using JSON PARSE
-
Verify round-trip fidelity: compare each field against the original values. Display PASS or FAIL for each field.
-
Test error handling: deliberately parse malformed JSON (missing closing brace, invalid number) and verify your ON EXCEPTION handler catches it
This exercise validates that your JSON generation and parsing are symmetric — data survives the round-trip without loss or corruption. For financial data, this is a critical property.
🔗 Cross-Reference: This exercise combines the JSON techniques from sections 39.3 with the error handling patterns from section 39.15 and the testing discipline from Chapter 29.
39.19 MQ Dead Letter Queue Management
In production, messages that cannot be processed end up on the dead letter queue (DLQ). Managing the DLQ is an operational responsibility that COBOL developers must understand.
Why Messages End Up on the DLQ
Messages land on the DLQ for several reasons: - Poison messages: Malformed content that causes the processing program to fail repeatedly - Queue full: The target queue reached its maximum depth - Expired messages: The message's expiry time elapsed before it could be processed - Authority failures: The receiving program lacks permission to GET from the queue - Conversion errors: Data conversion (e.g., EBCDIC to UTF-8) failed
DLQ Handler Program
A dedicated COBOL program processes the DLQ, categorizing messages and routing them appropriately:
IDENTIFICATION DIVISION.
PROGRAM-ID. DLQPROC.
*
* Dead Letter Queue Processor
* Reads messages from the DLQ, categorizes them,
* and routes to appropriate action queues.
*
PROCEDURE DIVISION.
0000-MAIN.
PERFORM 1000-INITIALIZE
PERFORM 2000-PROCESS-DLQ
UNTIL WS-DLQ-EMPTY OR WS-MAX-REACHED
PERFORM 9000-WRITE-REPORT
STOP RUN.
2000-PROCESS-DLQ.
PERFORM 2100-GET-DLQ-MESSAGE
IF WS-MSG-RECEIVED
ADD 1 TO WS-DLQ-MSG-COUNT
PERFORM 2200-EXAMINE-DLQ-HEADER
EVALUATE WS-DLQ-REASON
WHEN MQRC-MSG-TOO-BIG-FOR-Q
PERFORM 3000-ROUTE-TO-OVERFLOW
WHEN MQRC-Q-FULL
PERFORM 3100-RETRY-ORIGINAL-QUEUE
WHEN MQRC-NOT-AUTHORIZED
PERFORM 3200-ROUTE-TO-SECURITY-TEAM
WHEN OTHER
PERFORM 3300-ROUTE-TO-INVESTIGATION
END-EVALUATE
END-IF.
2200-EXAMINE-DLQ-HEADER.
* The Dead Letter Header (MQDLH) is prepended
* to messages on the DLQ
MOVE WS-MSG-BUFFER(1:LENGTH OF MQDLH)
TO WS-DLH
MOVE MQDLH-REASON OF WS-DLH
TO WS-DLQ-REASON
MOVE MQDLH-DESTQNAME OF WS-DLH
TO WS-ORIGINAL-QUEUE
MOVE MQDLH-DESTQMGRNAME OF WS-DLH
TO WS-ORIGINAL-QMGR
DISPLAY 'DLQ Message: reason='
WS-DLQ-REASON
' original queue='
WS-ORIGINAL-QUEUE.
At GlobalBank, the DLQ processor runs every 15 minutes as a scheduled batch job. It generates a report showing the count and categorization of DLQ messages. If the count exceeds a threshold (10 messages in an hour), it pages the integration support team.
📊 DLQ Monitoring Dashboard
GLOBALBANK MQ DEAD LETTER QUEUE REPORT
Report Time: 2026-03-11 02:15:00
═══════════════════════════════════════
Queue Manager: GBANK.PROD.QM01
Current DLQ Depth: 3 messages
Message 1:
Original Queue: GBANK.TXN.NOTIFY
Reason: 2053 (MQRC-Q-FULL)
Put Time: 2026-03-11 01:47:22
Age: 28 minutes
Action: AUTO-RETRY (target queue depth cleared)
Message 2:
Original Queue: GBANK.FEED.EXTERNAL
Reason: 2110 (MQRC-FORMAT-ERROR)
Put Time: 2026-03-11 01:52:15
Age: 23 minutes
Action: ROUTE-TO-INVESTIGATION
Message 3:
Original Queue: GBANK.ALERT.FRAUD
Reason: 2035 (MQRC-NOT-AUTHORIZED)
Put Time: 2026-03-11 02:01:44
Age: 13 minutes
Action: ROUTE-TO-SECURITY-TEAM *** ALERT ***
⚠️ Operational Rule: Never ignore the DLQ. An accumulating DLQ is a symptom of an integration problem. At MedClaim, James Okafor discovered that 47 denied claims had been silently dropped because the notification queue was full and messages went to the DLQ unnoticed for three days. After that incident, he implemented automated DLQ monitoring with a 15-minute polling interval and a zero-tolerance alerting threshold for the claims notification queues.
39.20 MedClaim: Event-Driven Claims Status Notification
To illustrate a complete event-driven integration, let us trace a claim status notification from adjudication through to the member's mobile app.
The Event Flow
Step 1: CLM-ADJ adjudicates claim
→ Result: APPROVED, plan pays $1,000.00
Step 2: CLM-ADJ publishes event to MQ
→ Queue: MEDCL.CLAIM.EVENTS
→ Message: JSON with claim result
Step 3: Cloud event bridge reads from MQ
→ Forwards to Kafka topic: claim-events
Step 4: Notification service (cloud, Node.js)
→ Reads from Kafka
→ Looks up member notification preferences
→ Sends push notification to mobile app
Step 5: Member sees on phone:
"Your claim for Dr. Smith visit on 3/5
has been approved. Plan pays: $1,000.00.
Your responsibility: $250.00."
The COBOL Event Publisher
4800-PUBLISH-CLAIM-EVENT.
* -----------------------------------------------
* After adjudication, publish result as event
* -----------------------------------------------
EVALUATE CLM-ADJ-RESULT
WHEN 'APPROVED'
MOVE 'CLAIM_APPROVED' TO WS-EVENT-TYPE
WHEN 'DENIED'
MOVE 'CLAIM_DENIED' TO WS-EVENT-TYPE
WHEN 'PENDED'
MOVE 'CLAIM_PENDED' TO WS-EVENT-TYPE
END-EVALUATE
STRING
'{"eventType":"' WS-EVENT-TYPE '",'
'"claimId":"' CLM-CLAIM-ID '",'
'"memberId":"' CLM-MEMBER-ID '",'
'"serviceDate":"' WS-ISO-SVC-DATE '",'
'"provider":"' CLM-PROVIDER-NAME '",'
'"chargedAmount":' WS-CHARGED-DISP ','
'"planPays":' WS-PLAN-PAYS-DISP ','
'"memberOwes":' WS-MEMBER-OWES-DISP ','
'"adjDate":"' WS-ISO-TIMESTAMP '",'
'"eventId":"' WS-EVENT-UUID '"}'
DELIMITED SIZE
INTO WS-EVENT-BUFFER
POINTER WS-EVENT-LENGTH
SUBTRACT 1 FROM WS-EVENT-LENGTH
* Set message properties for routing
MOVE MQMD-DEFAULT TO WS-EVENT-MQMD
MOVE MQFMT-STRING TO MQMD-FORMAT OF WS-EVENT-MQMD
MOVE 1208 TO MQMD-CCSID OF WS-EVENT-MQMD
MOVE MQPER-PERSISTENT
TO MQMD-PERSISTENCE OF WS-EVENT-MQMD
MOVE MQMT-DATAGRAM
TO MQMD-MSGTYPE OF WS-EVENT-MQMD
CALL 'MQPUT'
USING WS-HCONN
WS-HOBJ-EVENTS
WS-EVENT-MQMD
WS-PMO
WS-EVENT-LENGTH
WS-EVENT-BUFFER
WS-COMPCODE
WS-REASON
IF WS-COMPCODE = MQCC-OK
ADD 1 TO WS-EVENTS-PUBLISHED
ELSE
DISPLAY 'EVENT PUBLISH FAILED: reason='
WS-REASON
ADD 1 TO WS-EVENT-ERRORS
* Do NOT fail the adjudication because
* the notification failed — the claim is
* already adjudicated. Log and continue.
END-IF.
💡 Key Insight: Notice that the event publication failure does not cause the adjudication to fail. The claim has already been adjudicated — the event is a notification, not a transaction. If the event fails to publish, the claim is still correctly processed; the member just does not get the push notification. This is a deliberate design decision: the system of record (adjudication result in DB2) must not be affected by downstream notification failures.
This pattern — fire-and-forget events that do not affect the core transaction — is fundamental to reliable event-driven architecture.
39.21 Integration Security Patterns
Exposing COBOL services to the outside world introduces security concerns that did not exist when COBOL only communicated through internal files and CICS screens.
Authentication at the Boundary
COBOL programs should never handle authentication directly. Authentication is handled by the infrastructure layers above:
Mobile App → API Gateway (validates JWT token)
→ z/OS Connect (maps authenticated identity)
→ CICS (RACF security check)
→ COBOL program (receives validated identity in COMMAREA)
The COBOL program receives only the authenticated user identity, never the raw credentials:
* COBOL program receives pre-authenticated identity
05 COMM-USER-ID PIC X(08).
05 COMM-USER-ROLE PIC X(10).
*
* Authorization check in COBOL
1050-CHECK-AUTHORIZATION.
EVALUATE COMM-FUNCTION
WHEN 'INQUIRY '
* All roles can inquire
CONTINUE
WHEN 'UPDATE '
* Only MANAGER and ADMIN can update
IF COMM-USER-ROLE NOT = 'MANAGER'
AND COMM-USER-ROLE NOT = 'ADMIN'
MOVE '03' TO COMM-RETURN-CODE
MOVE 'Not authorized for updates'
TO COMM-ERROR-MSG
EXEC CICS RETURN END-EXEC
END-IF
WHEN 'DELETE '
* Only ADMIN can delete
IF COMM-USER-ROLE NOT = 'ADMIN'
MOVE '03' TO COMM-RETURN-CODE
MOVE 'Not authorized for deletes'
TO COMM-ERROR-MSG
EXEC CICS RETURN END-EXEC
END-IF
END-EVALUATE.
Input Validation for API-Exposed Programs
When COBOL programs receive input from external APIs, they must validate rigorously. External input cannot be trusted:
1100-VALIDATE-API-INPUT.
* -----------------------------------------------
* Validate all fields from API request
* Defense against malformed/malicious input
* -----------------------------------------------
* Account number: must be 10 numeric digits
INSPECT COMM-ACCT-NO TALLYING WS-NON-NUMERIC
FOR ALL CHARACTERS BEFORE INITIAL SPACE
IF WS-NON-NUMERIC > 0
OR COMM-ACCT-NO = SPACES
MOVE '04' TO COMM-RETURN-CODE
MOVE 'Invalid account number format'
TO COMM-ERROR-MSG
PERFORM 9000-RETURN-ERROR
END-IF
* Amount: must be positive and within range
IF COMM-AMOUNT NOT NUMERIC
OR COMM-AMOUNT <= 0
OR COMM-AMOUNT > 9999999.99
MOVE '04' TO COMM-RETURN-CODE
MOVE 'Invalid amount'
TO COMM-ERROR-MSG
PERFORM 9000-RETURN-ERROR
END-IF
* Function code: must be a known value
EVALUATE COMM-FUNCTION
WHEN 'INQUIRY '
WHEN 'UPDATE '
WHEN 'HISTORY '
CONTINUE
WHEN OTHER
MOVE '04' TO COMM-RETURN-CODE
MOVE 'Unknown function code'
TO COMM-ERROR-MSG
PERFORM 9000-RETURN-ERROR
END-EVALUATE.
⚠️ Security Principle: Never trust input from an API, even if the API gateway has already validated it. Defense in depth means validating at every layer. The COBOL program is the last line of defense before data reaches the database.
Audit Logging for API Access
Every API-initiated action should be logged for security audit:
9100-AUDIT-API-ACCESS.
MOVE FUNCTION CURRENT-DATE TO WS-AUDIT-TIMESTAMP
MOVE COMM-USER-ID TO WS-AUDIT-USER
MOVE COMM-FUNCTION TO WS-AUDIT-ACTION
MOVE COMM-ACCT-NO TO WS-AUDIT-RESOURCE
MOVE COMM-RETURN-CODE TO WS-AUDIT-RESULT
MOVE EIBTRMID TO WS-AUDIT-SOURCE
* EIBTRMID contains 'API ' for z/OS Connect calls
EXEC CICS WRITEQ TD
QUEUE('AUDT')
FROM(WS-AUDIT-RECORD)
LENGTH(LENGTH OF WS-AUDIT-RECORD)
END-EXEC.
At GlobalBank, all API-initiated transactions are logged to a CICS transient data queue (TDQ) that feeds into the bank's Security Information and Event Management (SIEM) system. Priya Kapoor mandated this logging as part of the API enablement project: "Every action through the API must be traceable to a user, a time, and a result. No exceptions."
39.22 Performance Tuning for High-Volume Integration
When COBOL services handle thousands of API calls per second, performance optimization becomes critical.
Connection Reuse in CICS
CICS automatically manages connection pooling for COBOL programs. Each CICS transaction reuses an existing thread rather than creating a new one. However, the COBOL program can still introduce performance bottlenecks:
* SLOW: Reading a DB2 table for every request
1000-SLOW-LOOKUP.
EXEC SQL
SELECT FEE_AMOUNT
INTO :WS-FEE-AMT
FROM FEE_SCHEDULE
WHERE FEE_CODE = :COMM-FEE-CODE
END-EXEC.
* FAST: Cache the fee table in CICS shared memory
1000-FAST-LOOKUP.
* Check CICS TS queue first (in-memory cache)
EXEC CICS READQ TS
QUEUE('FEETBL')
INTO(WS-FEE-TABLE)
ITEM(1)
RESP(WS-RESP)
END-EXEC
IF WS-RESP = DFHRESP(NORMAL)
* Cache hit — use cached fee table
PERFORM 1100-LOOKUP-IN-CACHE
ELSE
* Cache miss — load from DB2, cache for next time
PERFORM 1200-LOAD-AND-CACHE-FEE-TABLE
END-IF.
Message Batching for MQ
Instead of putting one message at a time, batch multiple messages under a single syncpoint:
4500-BATCH-MQPUT.
ADD 1 TO WS-BATCH-COUNT
CALL 'MQPUT'
USING WS-HCONN WS-HOBJ
WS-MSG-DESC WS-PMO
WS-BUFFER-LENGTH WS-MSG-BUFFER
WS-COMPCODE WS-REASON
IF WS-BATCH-COUNT >= WS-BATCH-SIZE
* Commit the batch
CALL 'MQCMIT' USING WS-HCONN
WS-COMPCODE
WS-REASON
MOVE 0 TO WS-BATCH-COUNT
END-IF.
Batching reduces the number of syncpoint operations, which are the most expensive part of persistent message handling.
📊 Performance Impact of Integration Patterns
| Pattern | Overhead per Call | Typical Throughput |
|---|---|---|
| CICS COMMAREA (local) | 0.5ms | 10,000+ TPS |
| CICS + z/OS Connect (REST) | 2-5ms | 2,000-5,000 TPS |
| MQ request/reply | 5-15ms | 500-2,000 TPS |
| CICS HTTP outbound (to cloud) | 20-100ms | 100-500 TPS |
| Batch file interface | N/A (latency in minutes/hours) | Millions/hour |
These numbers highlight a key architectural decision: for high-throughput, low-latency requirements, keep the call path short (CICS COMMAREA or z/OS Connect). For lower-throughput, reliability-focused requirements, use MQ.
Minimizing JSON Processing Overhead
JSON GENERATE and JSON PARSE add processing overhead compared to working with fixed COBOL records. For high-volume services, consider these optimizations:
Pre-allocate JSON buffers: Use a large enough buffer to avoid truncation errors, but not so large that you waste memory across thousands of concurrent transactions:
* Size the buffer based on the largest possible response
* Account inquiry: ~300 bytes JSON
* Transaction history: ~5,000 bytes JSON
EVALUATE COMM-FUNCTION
WHEN 'INQUIRY '
MOVE 512 TO WS-JSON-BUFFER-SIZE
WHEN 'HISTORY '
MOVE 8192 TO WS-JSON-BUFFER-SIZE
END-EVALUATE.
Use SUPPRESS to reduce JSON size: Every byte saved in the response reduces network time:
JSON GENERATE WS-JSON-OUTPUT
FROM WS-API-RESPONSE
COUNT IN WS-JSON-LENGTH
SUPPRESS WHEN ZERO
SUPPRESS WHEN SPACES
NAME OF ...
END-JSON.
Cache frequently generated JSON: If the same response is requested repeatedly (e.g., a product catalog or fee schedule), cache the generated JSON rather than regenerating it for each request.
Avoid STRING for JSON construction in high-volume paths: While STRING is flexible, JSON GENERATE is typically faster because the compiler optimizes the generation path. Use STRING only when JSON GENERATE cannot produce the required structure.
Connection Timeout and Circuit Breaker Patterns
When COBOL calls external APIs, network failures are inevitable. The circuit breaker pattern prevents cascading failures:
01 WS-CIRCUIT-BREAKER.
05 CB-FAILURE-COUNT PIC 9(03) VALUE 0.
05 CB-THRESHOLD PIC 9(03) VALUE 5.
05 CB-STATE PIC X(06) VALUE 'CLOSED'.
88 CB-IS-CLOSED VALUE 'CLOSED'.
88 CB-IS-OPEN VALUE 'OPEN '.
88 CB-IS-HALF-OPEN VALUE 'HALF '.
05 CB-LAST-FAILURE-TIME PIC X(26).
05 CB-COOLDOWN-SECONDS PIC 9(04) VALUE 60.
6000-CALL-WITH-CIRCUIT-BREAKER.
IF CB-IS-OPEN
* Check if cooldown period has elapsed
PERFORM 6050-CHECK-COOLDOWN
IF CB-IS-OPEN
MOVE '02' TO COMM-RETURN-CODE
MOVE 'Service temporarily unavailable'
TO COMM-ERROR-MSG
EXIT PARAGRAPH
END-IF
END-IF
* Attempt the external call
PERFORM 6100-MAKE-EXTERNAL-CALL
IF WS-CALL-SUCCESSFUL
IF CB-IS-HALF-OPEN
SET CB-IS-CLOSED TO TRUE
MOVE 0 TO CB-FAILURE-COUNT
END-IF
ELSE
ADD 1 TO CB-FAILURE-COUNT
IF CB-FAILURE-COUNT >= CB-THRESHOLD
SET CB-IS-OPEN TO TRUE
MOVE FUNCTION CURRENT-DATE
TO CB-LAST-FAILURE-TIME
DISPLAY 'CIRCUIT BREAKER OPEN: '
CB-FAILURE-COUNT ' failures'
END-IF
END-IF.
The circuit breaker prevents the COBOL program from repeatedly calling a failed external service, which would waste time and accumulate timeout delays. After 5 failures, the circuit opens and subsequent calls immediately return an error. After a cooldown period, the circuit enters half-open state and allows one test call through.
✅ Best Practice: Every external API call from COBOL should have a timeout, a retry limit, and a circuit breaker. Without these protections, a single failed external service can cause cascading timeouts across the entire CICS region, degrading performance for all transactions — not just the ones calling the failed service.
39.24 Chapter Summary
COBOL's isolation is a thing of the past. Through IBM MQ, JSON/XML processing, REST APIs, and event-driven patterns, COBOL programs integrate seamlessly with the modern technology stack. The key realization is that integration happens at the boundary — the COBOL business logic stays the same, wrapped in new interfaces that translate between COBOL's fixed-format world and the flexible formats of the web.
At GlobalBank, Priya Kapoor's architecture demonstrates this principle: the same ACCTINQ program that served 3270 terminals now serves mobile apps through REST APIs. The business logic did not change — the plumbing did. At MedClaim, the ELIGCHK program handles 50,000 daily eligibility checks through an API that healthcare providers access from their office management systems, with the same COBOL code that has been reliable for seven years.
Derek Washington's reaction after implementing his first MQ integration: "So the mainframe was not isolated — we just did not have the right pipes connected?"
Priya's response: "Exactly. The mainframe was always fast and reliable. Now the rest of the world can reach it."
🔗 Looking Ahead: Chapter 40 takes integration further — into containers, cloud platforms, CI/CD pipelines, and microservices architecture. If this chapter was about connecting COBOL to the modern world, the next chapter is about COBOL living in the modern world.