24 min read

> "The mainframe does not need to be an island. It just needs a bridge."

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:

  1. Connects to the queue manager
  2. Opens a queue for input
  3. Reads messages in a loop (with a 10-second timeout)
  4. Parses each message (assume simple key=value format)
  5. Processes the message (look up a record in a file)
  6. Builds a response message
  7. 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:

  1. Publishing events after transactions post (via MQ, as shown in section 39.8)
  2. Consuming events from external systems (new account applications from the web portal)
  3. 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:

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

  2. Populate the structure with sample data

  3. Generate JSON from the structure using JSON GENERATE with NAME clauses for camelCase field names

  4. Display the JSON output to SYSOUT

  5. Clear the original structure (INITIALIZE)

  6. Parse the JSON back into the same structure using JSON PARSE

  7. Verify round-trip fidelity: compare each field against the original values. Display PASS or FAIL for each field.

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