Case Study 2: CICS-to-Web Service Bridge

[IBM Enterprise COBOL]

Background

Summit Valley Credit Union has operated its core banking system on CICS for over thirty years. The system processes member accounts, loan applications, and teller transactions through a suite of COBOL programs that collectively manage over $4 billion in assets. The credit union's executive team has approved a digital transformation initiative to build a mobile banking application and a partner API platform. Both require real-time access to account data that currently lives behind the CICS green-screen wall.

Rather than rewriting the core banking logic -- which has been refined, audited, and proven reliable over decades -- the architecture team has decided to expose the existing COBOL programs as web services. The first service to be built is an Account Balance Inquiry API that accepts an account number via HTTP, retrieves the balance from the core system, and returns the result as a JSON response.

The implementation uses CICS Web Support to receive HTTP requests, CICS channels and containers to pass data between programs, and manual JSON formatting in COBOL to construct the response payload. This approach requires no changes to the existing core banking programs -- the web service is a new COBOL program that acts as a bridge between the HTTP world and the CICS transaction processing world.


Problem Statement

The Account Balance Inquiry API must meet the following requirements:

  1. Endpoint: GET /api/v1/accounts/{accountId}/balance
  2. Request: The account ID is passed as a path parameter in the URL
  3. Response: A JSON payload containing account number, account type, customer name, current balance, available balance, last activity date, and a status indicator
  4. Error Handling: Return appropriate HTTP status codes (200 for success, 404 for account not found, 400 for invalid request, 500 for internal errors) with JSON error bodies
  5. Performance: Response time under 200 milliseconds for the 95th percentile
  6. Security: Validate the API key passed in the X-API-Key HTTP header

The JSON response format for a successful inquiry:

{
  "accountId": "100045678901",
  "accountType": "CHECKING",
  "customerName": "JOHNSON, ROBERT M",
  "currentBalance": 15247.83,
  "availableBalance": 14747.83,
  "lastActivityDate": "2026-02-08",
  "currency": "USD",
  "status": "ACTIVE",
  "responseTimestamp": "2026-02-10T14:30:22.000Z",
  "requestId": "REQ-20260210-143022-0001"
}

The JSON error response format:

{
  "error": {
    "code": "ACCOUNT_NOT_FOUND",
    "message": "No account found with ID 100045678901",
    "requestId": "REQ-20260210-143025-0002",
    "timestamp": "2026-02-10T14:30:25.000Z"
  }
}

Architecture

Request Flow

  Mobile App / Partner System
       |
       | HTTPS GET /api/v1/accounts/{id}/balance
       | Header: X-API-Key: sk_live_xxx
       |
       v
  CICS TS Web Support (URIMAP / TCPIPSERVICE)
       |
       v
  ACBLWEB0 (Web Service Bridge Program)
       |
       |-- 1. Extract account ID from URL path
       |-- 2. Validate API key from HTTP header
       |-- 3. PUT account ID into channel container
       |-- 4. LINK to ACBLCORE (core inquiry program)
       |-- 5. GET response data from channel container
       |-- 6. Format JSON response
       |-- 7. WEB SEND HTTP response
       |
       v
  ACBLCORE (Core Banking Inquiry - existing program)
       |
       |-- 1. GET account ID from channel container
       |-- 2. Read DB2 ACCOUNT_MASTER table
       |-- 3. PUT response data into channel container
       |-- 4. RETURN to caller

CICS Resource Definitions

The web service requires several CICS resource definitions:

TCPIPSERVICE -- Defines the TCP/IP listener:

TCPIPSERVICE(WEBAPI)
    PORTNUMBER(8443)
    PROTOCOL(HTTP)
    TRANSACTION(CWBA)
    SSL(YES)
    CERTIFICATE(SVCERT01)
    STATUS(OPEN)

URIMAP -- Maps the URL pattern to the COBOL program:

URIMAP(ACBLURI)
    USAGE(SERVER)
    SCHEME(HTTPS)
    HOST(*)
    PATH(/api/v1/accounts/*)
    PROGRAM(ACBLWEB0)
    TCPIPSERVICE(WEBAPI)

PROGRAM definitions for both programs:

PROGRAM(ACBLWEB0)
    LANGUAGE(COBOL)
    DATALOCATION(ANY)
    EXECKEY(USER)
    STATUS(ENABLED)

PROGRAM(ACBLCORE)
    LANGUAGE(COBOL)
    DATALOCATION(ANY)
    EXECKEY(USER)
    STATUS(ENABLED)

The Web Service Bridge Program

       IDENTIFICATION DIVISION.
       PROGRAM-ID. ACBLWEB0.
      *================================================================*
      * SUMMIT VALLEY CREDIT UNION                                     *
      * ACCOUNT BALANCE INQUIRY - WEB SERVICE BRIDGE                   *
      *                                                                *
      * This program receives HTTP requests via CICS Web Support,      *
      * delegates to the core banking inquiry program using            *
      * channels/containers, and formats the response as JSON.         *
      *                                                                *
      * Endpoint: GET /api/v1/accounts/{accountId}/balance             *
      * Response: application/json                                     *
      *================================================================*

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01  WS-PROGRAM-ID              PIC X(08) VALUE 'ACBLWEB0'.
       01  WS-CORE-PROGRAM            PIC X(08) VALUE 'ACBLCORE'.
       01  WS-CHANNEL-NAME            PIC X(16) VALUE 'ACBL-CHANNEL'.

      * HTTP request fields
       01  WS-HTTP-REQUEST.
           05  WS-HTTP-METHOD         PIC X(10).
           05  WS-HTTP-METHOD-LEN     PIC S9(08) COMP.
           05  WS-HTTP-PATH           PIC X(256).
           05  WS-HTTP-PATH-LEN       PIC S9(08) COMP.
           05  WS-API-KEY             PIC X(64).
           05  WS-API-KEY-LEN         PIC S9(08) COMP.
           05  WS-CONTENT-TYPE        PIC X(40).

      * Extracted fields
       01  WS-REQUEST-DATA.
           05  WS-ACCOUNT-ID          PIC X(12).
           05  WS-REQUEST-ID          PIC X(30).
           05  WS-REQUEST-VALID       PIC X VALUE 'N'.
               88  REQUEST-IS-VALID       VALUE 'Y'.
               88  REQUEST-NOT-VALID      VALUE 'N'.

      * Container data structures
       01  WS-INQUIRY-REQUEST.
           05  WS-INQ-ACCOUNT-ID      PIC X(12).
           05  WS-INQ-REQUEST-TYPE    PIC X(04) VALUE 'BALC'.

       01  WS-INQUIRY-RESPONSE.
           05  WS-RSP-RETURN-CODE     PIC 9(04).
               88  RSP-SUCCESS            VALUE 0000.
               88  RSP-NOT-FOUND          VALUE 0404.
               88  RSP-DB-ERROR           VALUE 0500.
           05  WS-RSP-ACCOUNT-ID      PIC X(12).
           05  WS-RSP-ACCT-TYPE       PIC X(02).
           05  WS-RSP-CUST-NAME       PIC X(30).
           05  WS-RSP-CURRENT-BAL     PIC S9(11)V99 COMP-3.
           05  WS-RSP-AVAIL-BAL       PIC S9(11)V99 COMP-3.
           05  WS-RSP-LAST-ACTIVITY   PIC X(10).
           05  WS-RSP-STATUS          PIC X(01).
           05  WS-RSP-MESSAGE         PIC X(80).

      * JSON response construction
       01  WS-JSON-BUFFER             PIC X(2000).
       01  WS-JSON-LENGTH             PIC S9(08) COMP VALUE 0.
       01  WS-JSON-OFFSET             PIC S9(08) COMP VALUE 0.

      * JSON field formatting work areas
       01  WS-FMT-BALANCE             PIC -(11)9.99.
       01  WS-FMT-BAL-TRIMMED         PIC X(16).
       01  WS-ACCT-TYPE-NAME          PIC X(15).
       01  WS-STATUS-NAME             PIC X(10).

      * Timestamp fields
       01  WS-CURRENT-DATETIME.
           05  WS-CURR-DATE.
               10  WS-CURR-YEAR       PIC 9(04).
               10  WS-CURR-MONTH      PIC 9(02).
               10  WS-CURR-DAY        PIC 9(02).
           05  WS-CURR-TIME.
               10  WS-CURR-HOUR       PIC 9(02).
               10  WS-CURR-MIN        PIC 9(02).
               10  WS-CURR-SEC        PIC 9(02).
               10  WS-CURR-HUND       PIC 9(02).
           05  WS-CURR-GMT-OFFSET     PIC X(05).
       01  WS-ISO-TIMESTAMP           PIC X(24).

      * API key validation
       01  WS-VALID-API-KEYS.
           05  FILLER PIC X(32) VALUE 'sk_live_abc123def456ghi789jk'.
           05  FILLER PIC X(32) VALUE 'sk_live_mno012pqr345stu678vw'.
           05  FILLER PIC X(32) VALUE 'sk_live_xyz901abc234def567gh'.
       01  WS-VALID-API-KEYS-R REDEFINES WS-VALID-API-KEYS.
           05  WS-VALID-KEY PIC X(32) OCCURS 3 TIMES.
       01  WS-KEY-IDX                 PIC 9(02).
       01  WS-KEY-VALID               PIC X VALUE 'N'.
           88  API-KEY-IS-VALID           VALUE 'Y'.
           88  API-KEY-NOT-VALID          VALUE 'N'.

      * CICS response fields
       01  WS-RESP                    PIC S9(08) COMP.
       01  WS-RESP2                   PIC S9(08) COMP.
       01  WS-CONTAINER-LEN          PIC S9(08) COMP.

      * URL parsing work fields
       01  WS-PATH-SEGMENT            PIC X(50).
       01  WS-PARSE-IDX               PIC 9(05).
       01  WS-SEGMENT-START           PIC 9(05).
       01  WS-SEGMENT-COUNT           PIC 9(03).
       01  WS-SLASH-COUNT             PIC 9(03).

       01  WS-ABSTIME                 PIC S9(15) COMP-3.
       01  WS-SEQ-NUM                 PIC 9(04) VALUE 0.
       01  WS-SEQ-DISPLAY             PIC X(04).

       COPY DFHAID.

       PROCEDURE DIVISION.
       0000-MAIN-CONTROL.
           PERFORM 1000-EXTRACT-HTTP-REQUEST
           PERFORM 2000-VALIDATE-REQUEST
           IF REQUEST-IS-VALID
               PERFORM 3000-CALL-CORE-INQUIRY
               PERFORM 4000-FORMAT-JSON-RESPONSE
               PERFORM 5000-SEND-HTTP-RESPONSE
           END-IF
           EXEC CICS RETURN END-EXEC.

      *================================================================*
      * 1000 - EXTRACT HTTP REQUEST INFORMATION                        *
      *================================================================*
       1000-EXTRACT-HTTP-REQUEST.
      *    Get the current timestamp for the response
           MOVE FUNCTION CURRENT-DATE
               TO WS-CURRENT-DATETIME

      *    Build ISO 8601 timestamp
           STRING WS-CURR-YEAR '-' WS-CURR-MONTH '-'
               WS-CURR-DAY 'T' WS-CURR-HOUR ':'
               WS-CURR-MIN ':' WS-CURR-SEC '.000Z'
               DELIMITED BY SIZE
               INTO WS-ISO-TIMESTAMP

      *    Generate request ID
           ADD 1 TO WS-SEQ-NUM
           MOVE WS-SEQ-NUM TO WS-SEQ-DISPLAY
           STRING 'REQ-' WS-CURR-YEAR WS-CURR-MONTH
               WS-CURR-DAY '-' WS-CURR-HOUR WS-CURR-MIN
               WS-CURR-SEC '-' WS-SEQ-DISPLAY
               DELIMITED BY SIZE
               INTO WS-REQUEST-ID

      *    Extract HTTP method
           EXEC CICS WEB READ
               HTTPMETHOD(WS-HTTP-METHOD)
               METHODLENGTH(WS-HTTP-METHOD-LEN)
               RESP(WS-RESP)
           END-EXEC

      *    Extract URL path
           EXEC CICS WEB READ
               PATH(WS-HTTP-PATH)
               PATHLENGTH(WS-HTTP-PATH-LEN)
               RESP(WS-RESP)
           END-EXEC

      *    Extract API key from header
           MOVE 64 TO WS-API-KEY-LEN
           EXEC CICS WEB READ
               HTTPHEADER('X-API-Key')
               VALUE(WS-API-KEY)
               VALUELENGTH(WS-API-KEY-LEN)
               RESP(WS-RESP)
           END-EXEC
           IF WS-RESP NOT = DFHRESP(NORMAL)
               MOVE SPACES TO WS-API-KEY
           END-IF

      *    Parse account ID from URL path
      *    Path format: /api/v1/accounts/{accountId}/balance
      *    The account ID is the 5th segment (after 4 slashes)
           PERFORM 1100-PARSE-ACCOUNT-FROM-PATH.

       1100-PARSE-ACCOUNT-FROM-PATH.
           MOVE 0 TO WS-SLASH-COUNT
           MOVE 1 TO WS-SEGMENT-START
           MOVE SPACES TO WS-ACCOUNT-ID

           PERFORM VARYING WS-PARSE-IDX FROM 1 BY 1
               UNTIL WS-PARSE-IDX > WS-HTTP-PATH-LEN
               IF WS-HTTP-PATH(WS-PARSE-IDX:1) = '/'
                   ADD 1 TO WS-SLASH-COUNT
      *            After the 4th slash, capture the account ID
      *            /api/v1/accounts/{HERE}/balance
                   IF WS-SLASH-COUNT = 5
                       MOVE WS-SEGMENT-START TO WS-PARSE-IDX
                       COMPUTE WS-PARSE-IDX =
                           WS-PARSE-IDX
                   END-IF
                   COMPUTE WS-SEGMENT-START =
                       WS-PARSE-IDX + 1
               END-IF
           END-PERFORM

      *    Extract the account ID segment
      *    Find text between 4th and 5th slash
           MOVE 0 TO WS-SLASH-COUNT
           MOVE 0 TO WS-SEGMENT-START
           PERFORM VARYING WS-PARSE-IDX FROM 1 BY 1
               UNTIL WS-PARSE-IDX > WS-HTTP-PATH-LEN
               IF WS-HTTP-PATH(WS-PARSE-IDX:1) = '/'
                   ADD 1 TO WS-SLASH-COUNT
                   IF WS-SLASH-COUNT = 4
                       COMPUTE WS-SEGMENT-START =
                           WS-PARSE-IDX + 1
                   END-IF
                   IF WS-SLASH-COUNT = 5
      *                Extract the segment between slashes
                       MOVE WS-HTTP-PATH(
                           WS-SEGMENT-START:
                           WS-PARSE-IDX - WS-SEGMENT-START)
                           TO WS-ACCOUNT-ID
                       MOVE WS-HTTP-PATH-LEN
                           TO WS-PARSE-IDX
                   END-IF
               END-IF
           END-PERFORM

      *    If no 5th slash, take the rest of the path
           IF WS-ACCOUNT-ID = SPACES AND WS-SEGMENT-START > 0
               MOVE WS-HTTP-PATH(WS-SEGMENT-START:
                   WS-HTTP-PATH-LEN - WS-SEGMENT-START + 1)
                   TO WS-ACCOUNT-ID
           END-IF.

      *================================================================*
      * 2000 - VALIDATE THE REQUEST                                    *
      *================================================================*
       2000-VALIDATE-REQUEST.
           SET REQUEST-IS-VALID TO TRUE

      *    Validate HTTP method is GET
           IF WS-HTTP-METHOD(1:3) NOT = 'GET'
               PERFORM 2100-SEND-METHOD-ERROR
               SET REQUEST-NOT-VALID TO TRUE
           END-IF

      *    Validate API key
           IF REQUEST-IS-VALID
               PERFORM 2200-VALIDATE-API-KEY
               IF API-KEY-NOT-VALID
                   PERFORM 2300-SEND-AUTH-ERROR
                   SET REQUEST-NOT-VALID TO TRUE
               END-IF
           END-IF

      *    Validate account ID is present
           IF REQUEST-IS-VALID
               IF WS-ACCOUNT-ID = SPACES
                   PERFORM 2400-SEND-BAD-REQUEST
                   SET REQUEST-NOT-VALID TO TRUE
               END-IF
           END-IF.

       2100-SEND-METHOD-ERROR.
           MOVE SPACES TO WS-JSON-BUFFER
           STRING
               '{"error":{"code":"METHOD_NOT_ALLOWED",'
               '"message":"Only GET method is supported",'
               '"requestId":"' WS-REQUEST-ID '",'
               '"timestamp":"' WS-ISO-TIMESTAMP '"}}'
               DELIMITED BY SIZE
               INTO WS-JSON-BUFFER
           COMPUTE WS-JSON-LENGTH =
               FUNCTION LENGTH(
                   FUNCTION TRIM(WS-JSON-BUFFER TRAILING))
           EXEC CICS WEB SEND
               FROM(WS-JSON-BUFFER)
               FROMLENGTH(WS-JSON-LENGTH)
               MEDIATYPE('application/json')
               STATUSCODE(405)
               STATUSTEXT('Method Not Allowed')
               STATUSLEN(18)
               ACTION(IMMEDIATE)
               CLOSESTATUS(CLOSE)
               RESP(WS-RESP)
           END-EXEC.

       2200-VALIDATE-API-KEY.
           SET API-KEY-NOT-VALID TO TRUE
           PERFORM VARYING WS-KEY-IDX FROM 1 BY 1
               UNTIL WS-KEY-IDX > 3
               OR API-KEY-IS-VALID
               IF WS-API-KEY(1:32) =
                   WS-VALID-KEY(WS-KEY-IDX)
                   SET API-KEY-IS-VALID TO TRUE
               END-IF
           END-PERFORM.

       2300-SEND-AUTH-ERROR.
           MOVE SPACES TO WS-JSON-BUFFER
           STRING
               '{"error":{"code":"UNAUTHORIZED",'
               '"message":"Invalid or missing API key",'
               '"requestId":"' WS-REQUEST-ID '",'
               '"timestamp":"' WS-ISO-TIMESTAMP '"}}'
               DELIMITED BY SIZE
               INTO WS-JSON-BUFFER
           COMPUTE WS-JSON-LENGTH =
               FUNCTION LENGTH(
                   FUNCTION TRIM(WS-JSON-BUFFER TRAILING))
           EXEC CICS WEB SEND
               FROM(WS-JSON-BUFFER)
               FROMLENGTH(WS-JSON-LENGTH)
               MEDIATYPE('application/json')
               STATUSCODE(401)
               STATUSTEXT('Unauthorized')
               STATUSLEN(12)
               ACTION(IMMEDIATE)
               CLOSESTATUS(CLOSE)
               RESP(WS-RESP)
           END-EXEC.

       2400-SEND-BAD-REQUEST.
           MOVE SPACES TO WS-JSON-BUFFER
           STRING
               '{"error":{"code":"BAD_REQUEST",'
               '"message":"Account ID is required in URL path",'
               '"requestId":"' WS-REQUEST-ID '",'
               '"timestamp":"' WS-ISO-TIMESTAMP '"}}'
               DELIMITED BY SIZE
               INTO WS-JSON-BUFFER
           COMPUTE WS-JSON-LENGTH =
               FUNCTION LENGTH(
                   FUNCTION TRIM(WS-JSON-BUFFER TRAILING))
           EXEC CICS WEB SEND
               FROM(WS-JSON-BUFFER)
               FROMLENGTH(WS-JSON-LENGTH)
               MEDIATYPE('application/json')
               STATUSCODE(400)
               STATUSTEXT('Bad Request')
               STATUSLEN(11)
               ACTION(IMMEDIATE)
               CLOSESTATUS(CLOSE)
               RESP(WS-RESP)
           END-EXEC.

      *================================================================*
      * 3000 - CALL CORE INQUIRY VIA CHANNEL/CONTAINERS               *
      *================================================================*
       3000-CALL-CORE-INQUIRY.
      *    Prepare the inquiry request container
           MOVE WS-ACCOUNT-ID TO WS-INQ-ACCOUNT-ID
           MOVE 'BALC'        TO WS-INQ-REQUEST-TYPE

      *    Put request data into a container on the channel
           EXEC CICS PUT CONTAINER('INQ-REQUEST')
               FROM(WS-INQUIRY-REQUEST)
               FLENGTH(LENGTH OF WS-INQUIRY-REQUEST)
               CHANNEL(WS-CHANNEL-NAME)
               RESP(WS-RESP)
           END-EXEC

           IF WS-RESP NOT = DFHRESP(NORMAL)
               PERFORM 3900-CONTAINER-ERROR
           ELSE
      *        LINK to the core inquiry program with the channel
               EXEC CICS LINK
                   PROGRAM(WS-CORE-PROGRAM)
                   CHANNEL(WS-CHANNEL-NAME)
                   RESP(WS-RESP)
                   RESP2(WS-RESP2)
               END-EXEC

               IF WS-RESP NOT = DFHRESP(NORMAL)
                   PERFORM 3900-CONTAINER-ERROR
               ELSE
      *            Retrieve the response from the container
                   MOVE LENGTH OF WS-INQUIRY-RESPONSE
                       TO WS-CONTAINER-LEN
                   EXEC CICS GET CONTAINER('INQ-RESPONSE')
                       CHANNEL(WS-CHANNEL-NAME)
                       INTO(WS-INQUIRY-RESPONSE)
                       FLENGTH(WS-CONTAINER-LEN)
                       RESP(WS-RESP)
                   END-EXEC
               END-IF
           END-IF.

       3900-CONTAINER-ERROR.
           MOVE 0500 TO WS-RSP-RETURN-CODE
           MOVE 'Internal system error' TO WS-RSP-MESSAGE.

      *================================================================*
      * 4000 - FORMAT JSON RESPONSE                                    *
      *================================================================*
       4000-FORMAT-JSON-RESPONSE.
           MOVE SPACES TO WS-JSON-BUFFER

           EVALUATE TRUE
               WHEN RSP-SUCCESS
                   PERFORM 4100-FORMAT-SUCCESS-JSON
               WHEN RSP-NOT-FOUND
                   PERFORM 4200-FORMAT-NOT-FOUND-JSON
               WHEN RSP-DB-ERROR
                   PERFORM 4300-FORMAT-ERROR-JSON
               WHEN OTHER
                   PERFORM 4300-FORMAT-ERROR-JSON
           END-EVALUATE.

       4100-FORMAT-SUCCESS-JSON.
      *    Map account type code to display name
           EVALUATE WS-RSP-ACCT-TYPE
               WHEN 'CK'  MOVE 'CHECKING'     TO WS-ACCT-TYPE-NAME
               WHEN 'SV'  MOVE 'SAVINGS'      TO WS-ACCT-TYPE-NAME
               WHEN 'MM'  MOVE 'MONEY_MARKET' TO WS-ACCT-TYPE-NAME
               WHEN 'CD'  MOVE 'CERTIFICATE'  TO WS-ACCT-TYPE-NAME
               WHEN 'LN'  MOVE 'LOAN'         TO WS-ACCT-TYPE-NAME
               WHEN OTHER MOVE 'UNKNOWN'       TO WS-ACCT-TYPE-NAME
           END-EVALUATE

      *    Map status code to display name
           EVALUATE WS-RSP-STATUS
               WHEN 'O'   MOVE 'ACTIVE'     TO WS-STATUS-NAME
               WHEN 'F'   MOVE 'FROZEN'     TO WS-STATUS-NAME
               WHEN 'D'   MOVE 'DORMANT'    TO WS-STATUS-NAME
               WHEN 'C'   MOVE 'CLOSED'     TO WS-STATUS-NAME
               WHEN OTHER MOVE 'UNKNOWN'    TO WS-STATUS-NAME
           END-EVALUATE

      *    Format current balance (remove leading spaces)
           MOVE WS-RSP-CURRENT-BAL TO WS-FMT-BALANCE
           MOVE FUNCTION TRIM(WS-FMT-BALANCE)
               TO WS-FMT-BAL-TRIMMED

      *    Build the complete JSON response
      *    The JSON is constructed in a single STRING statement
      *    using FUNCTION TRIM to strip trailing spaces from
      *    each COBOL PIC X field before insertion.
           INITIALIZE WS-JSON-BUFFER

           STRING
               '{"accountId":"'
               FUNCTION TRIM(WS-RSP-ACCOUNT-ID)
               '","accountType":"'
               FUNCTION TRIM(WS-ACCT-TYPE-NAME)
               '","customerName":"'
               FUNCTION TRIM(WS-RSP-CUST-NAME)
               '","currentBalance":'
               FUNCTION TRIM(WS-FMT-BAL-TRIMMED)
               ','
               DELIMITED BY SIZE
               INTO WS-JSON-BUFFER
           END-STRING

      *    Format available balance and append remaining fields
           MOVE WS-RSP-AVAIL-BAL TO WS-FMT-BALANCE
           MOVE FUNCTION TRIM(WS-FMT-BALANCE)
               TO WS-FMT-BAL-TRIMMED

           STRING
               WS-JSON-BUFFER DELIMITED BY '  '
               '"availableBalance":'
               FUNCTION TRIM(WS-FMT-BAL-TRIMMED)
               ',"lastActivityDate":"'
               FUNCTION TRIM(WS-RSP-LAST-ACTIVITY)
               '","currency":"USD",'
               '"status":"'
               FUNCTION TRIM(WS-STATUS-NAME)
               '","responseTimestamp":"'
               WS-ISO-TIMESTAMP
               '","requestId":"'
               FUNCTION TRIM(WS-REQUEST-ID)
               '"}'
               DELIMITED BY SIZE
               INTO WS-JSON-BUFFER
           END-STRING

           COMPUTE WS-JSON-LENGTH =
               FUNCTION LENGTH(
                   FUNCTION TRIM(WS-JSON-BUFFER TRAILING)).

       4200-FORMAT-NOT-FOUND-JSON.
           MOVE SPACES TO WS-JSON-BUFFER
           STRING
               '{"error":{'
               '"code":"ACCOUNT_NOT_FOUND",'
               '"message":"No account found with ID '
                   FUNCTION TRIM(WS-ACCOUNT-ID) '",'
               '"requestId":"'
                   FUNCTION TRIM(WS-REQUEST-ID) '",'
               '"timestamp":"' WS-ISO-TIMESTAMP '"'
               '}}'
               DELIMITED BY SIZE
               INTO WS-JSON-BUFFER
           END-STRING
           COMPUTE WS-JSON-LENGTH =
               FUNCTION LENGTH(
                   FUNCTION TRIM(WS-JSON-BUFFER TRAILING)).

       4300-FORMAT-ERROR-JSON.
           MOVE SPACES TO WS-JSON-BUFFER
           STRING
               '{"error":{'
               '"code":"INTERNAL_ERROR",'
               '"message":"'
                   FUNCTION TRIM(WS-RSP-MESSAGE) '",'
               '"requestId":"'
                   FUNCTION TRIM(WS-REQUEST-ID) '",'
               '"timestamp":"' WS-ISO-TIMESTAMP '"'
               '}}'
               DELIMITED BY SIZE
               INTO WS-JSON-BUFFER
           END-STRING
           COMPUTE WS-JSON-LENGTH =
               FUNCTION LENGTH(
                   FUNCTION TRIM(WS-JSON-BUFFER TRAILING)).

      *================================================================*
      * 5000 - SEND HTTP RESPONSE                                      *
      *================================================================*
       5000-SEND-HTTP-RESPONSE.
           EVALUATE TRUE
               WHEN RSP-SUCCESS
                   EXEC CICS WEB SEND
                       FROM(WS-JSON-BUFFER)
                       FROMLENGTH(WS-JSON-LENGTH)
                       MEDIATYPE('application/json')
                       STATUSCODE(200)
                       STATUSTEXT('OK')
                       STATUSLEN(2)
                       ACTION(IMMEDIATE)
                       CLOSESTATUS(CLOSE)
                       RESP(WS-RESP)
                   END-EXEC
               WHEN RSP-NOT-FOUND
                   EXEC CICS WEB SEND
                       FROM(WS-JSON-BUFFER)
                       FROMLENGTH(WS-JSON-LENGTH)
                       MEDIATYPE('application/json')
                       STATUSCODE(404)
                       STATUSTEXT('Not Found')
                       STATUSLEN(9)
                       ACTION(IMMEDIATE)
                       CLOSESTATUS(CLOSE)
                       RESP(WS-RESP)
                   END-EXEC
               WHEN OTHER
                   EXEC CICS WEB SEND
                       FROM(WS-JSON-BUFFER)
                       FROMLENGTH(WS-JSON-LENGTH)
                       MEDIATYPE('application/json')
                       STATUSCODE(500)
                       STATUSTEXT('Internal Server Error')
                       STATUSLEN(21)
                       ACTION(IMMEDIATE)
                       CLOSESTATUS(CLOSE)
                       RESP(WS-RESP)
                   END-EXEC
           END-EVALUATE.

The Core Inquiry Program

The core inquiry program is a standalone COBOL program that can be called via LINK with a channel. It reads DB2 and returns the result through a container. Critically, this program has no HTTP awareness -- it works identically whether called from the web bridge, from a CICS terminal program, or from the CICS Transaction Gateway:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. ACBLCORE.
      *================================================================*
      * SUMMIT VALLEY CREDIT UNION                                     *
      * ACCOUNT BALANCE INQUIRY - CORE BUSINESS LOGIC                  *
      *                                                                *
      * This program is DPL-capable (no terminal I/O). It reads       *
      * the account from DB2 and returns data via channel/containers.  *
      * It can be called from CICS terminal programs, web services,    *
      * or the CICS Transaction Gateway.                               *
      *================================================================*

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01  WS-CHANNEL-NAME            PIC X(16).

      * Container data structures
       01  WS-INQUIRY-REQUEST.
           05  WS-INQ-ACCOUNT-ID      PIC X(12).
           05  WS-INQ-REQUEST-TYPE    PIC X(04).

       01  WS-INQUIRY-RESPONSE.
           05  WS-RSP-RETURN-CODE     PIC 9(04).
           05  WS-RSP-ACCOUNT-ID      PIC X(12).
           05  WS-RSP-ACCT-TYPE       PIC X(02).
           05  WS-RSP-CUST-NAME       PIC X(30).
           05  WS-RSP-CURRENT-BAL     PIC S9(11)V99 COMP-3.
           05  WS-RSP-AVAIL-BAL       PIC S9(11)V99 COMP-3.
           05  WS-RSP-LAST-ACTIVITY   PIC X(10).
           05  WS-RSP-STATUS          PIC X(01).
           05  WS-RSP-MESSAGE         PIC X(80).

      * DB2 host variables
       01  WS-DB-FIELDS.
           05  WS-DB-CUST-NAME        PIC X(30).
           05  WS-DB-ACCT-TYPE        PIC X(02).
           05  WS-DB-STATUS            PIC X(01).
           05  WS-DB-CURRENT-BAL       PIC S9(11)V99 COMP-3.
           05  WS-DB-AVAIL-BAL         PIC S9(11)V99 COMP-3.
           05  WS-DB-LAST-ACTIVITY     PIC X(10).

           EXEC SQL INCLUDE SQLCA END-EXEC.

       01  WS-RESP                    PIC S9(08) COMP.
       01  WS-CONTAINER-LEN          PIC S9(08) COMP.

       PROCEDURE DIVISION.
       0000-MAIN.
      *    Determine the channel name
           EXEC CICS ASSIGN
               CHANNEL(WS-CHANNEL-NAME)
           END-EXEC

      *    Get the request from the container
           MOVE LENGTH OF WS-INQUIRY-REQUEST
               TO WS-CONTAINER-LEN
           EXEC CICS GET CONTAINER('INQ-REQUEST')
               CHANNEL(WS-CHANNEL-NAME)
               INTO(WS-INQUIRY-REQUEST)
               FLENGTH(WS-CONTAINER-LEN)
               RESP(WS-RESP)
           END-EXEC

           IF WS-RESP NOT = DFHRESP(NORMAL)
               MOVE 0500 TO WS-RSP-RETURN-CODE
               MOVE 'Failed to read request container'
                   TO WS-RSP-MESSAGE
           ELSE
               PERFORM 1000-QUERY-ACCOUNT
           END-IF

      *    Put the response into a container
           EXEC CICS PUT CONTAINER('INQ-RESPONSE')
               FROM(WS-INQUIRY-RESPONSE)
               FLENGTH(LENGTH OF WS-INQUIRY-RESPONSE)
               CHANNEL(WS-CHANNEL-NAME)
               RESP(WS-RESP)
           END-EXEC

           EXEC CICS RETURN END-EXEC.

       1000-QUERY-ACCOUNT.
           EXEC SQL
               SELECT CUST_NAME, ACCT_TYPE, ACCT_STATUS,
                      CURRENT_BAL, AVAILABLE_BAL,
                      CHAR(LAST_ACTIVITY, ISO)
               INTO :WS-DB-CUST-NAME,
                    :WS-DB-ACCT-TYPE,
                    :WS-DB-STATUS,
                    :WS-DB-CURRENT-BAL,
                    :WS-DB-AVAIL-BAL,
                    :WS-DB-LAST-ACTIVITY
               FROM ACCOUNT_MASTER
               WHERE ACCT_NUMBER = :WS-INQ-ACCOUNT-ID
           END-EXEC

           EVALUATE SQLCODE
               WHEN 0
                   MOVE 0000 TO WS-RSP-RETURN-CODE
                   MOVE WS-INQ-ACCOUNT-ID
                       TO WS-RSP-ACCOUNT-ID
                   MOVE WS-DB-CUST-NAME
                       TO WS-RSP-CUST-NAME
                   MOVE WS-DB-ACCT-TYPE
                       TO WS-RSP-ACCT-TYPE
                   MOVE WS-DB-STATUS
                       TO WS-RSP-STATUS
                   MOVE WS-DB-CURRENT-BAL
                       TO WS-RSP-CURRENT-BAL
                   MOVE WS-DB-AVAIL-BAL
                       TO WS-RSP-AVAIL-BAL
                   MOVE WS-DB-LAST-ACTIVITY
                       TO WS-RSP-LAST-ACTIVITY
                   MOVE 'Success' TO WS-RSP-MESSAGE
               WHEN +100
                   MOVE 0404 TO WS-RSP-RETURN-CODE
                   MOVE WS-INQ-ACCOUNT-ID
                       TO WS-RSP-ACCOUNT-ID
                   MOVE 'Account not found'
                       TO WS-RSP-MESSAGE
               WHEN OTHER
                   MOVE 0500 TO WS-RSP-RETURN-CODE
                   STRING 'DB2 error. SQLCODE=' SQLCODE
                       DELIMITED BY SIZE
                       INTO WS-RSP-MESSAGE
           END-EVALUATE.

Solution Walkthrough

Separation of Concerns

The architecture cleanly separates three responsibilities:

  1. HTTP handling (ACBLWEB0) -- Parsing URLs, reading headers, formatting JSON, sending HTTP responses. This program knows about the web but not about databases.

  2. Business logic (ACBLCORE) -- Querying the database and applying business rules. This program knows about accounts but not about HTTP.

  3. Data transport (Channels/Containers) -- The channel mechanism decouples the web layer from the business layer. The web bridge puts a request in a container and gets a response from a container, without knowing how the core program processes it.

This separation means the core inquiry program can be reused by any future caller -- a CICS terminal program, a batch program via CICS Transaction Gateway, a Node.js application via JCICS, or another web service endpoint. No changes to ACBLCORE are required.

Channel/Container vs. COMMAREA

The design uses channels and containers rather than COMMAREA for several reasons:

  • Named data areas: The containers INQ-REQUEST and INQ-RESPONSE are self-documenting. A COMMAREA is a single, opaque block of bytes.
  • Independent sizing: The request container is 16 bytes; the response is over 140 bytes. With COMMAREA, both would share a single block sized to the larger of the two.
  • Extensibility: Adding a new container (e.g., AUDIT-LOG) requires no changes to the existing request/response structure.
  • Discovery: A future debugging tool can browse the channel's containers to see what data was exchanged, without needing to know the COMMAREA layout in advance.

JSON Construction in COBOL

Building JSON in COBOL requires careful string handling. The program uses STRING ... DELIMITED BY SIZE ... INTO ... to construct the JSON payload incrementally. Key challenges include:

  • Trailing spaces: COBOL PIC X fields are padded with spaces. The program uses FUNCTION TRIM to remove trailing spaces from every value before inserting it into the JSON.
  • Numeric formatting: Balances must appear as JSON numbers (no quotes, no leading spaces). The PIC -(11)9.99 edit mask formats the number, and FUNCTION TRIM removes the leading spaces.
  • Escaping: In a production system, customer names and other text fields would need JSON escaping (backslash for quotes, backslash for backslashes). This case study omits escaping for clarity but notes it as a required enhancement for production deployment.

In CICS TS V5.2 and later, the EXEC CICS TRANSFORM DATATODATA command with a JSON transformer can automate the conversion, eliminating manual JSON construction entirely.

Error Response Standardization

Every error response follows the same JSON structure with code, message, requestId, and timestamp fields. The requestId is generated at the beginning of each request and included in every response, enabling end-to-end tracing from the mobile app through the API gateway to the CICS log.

The HTTP status codes follow REST conventions: - 200 -- Successful inquiry - 400 -- Client error (missing account ID) - 401 -- Authentication failure (invalid API key) - 404 -- Account not found - 405 -- Wrong HTTP method - 500 -- Internal server error


Testing the Web Service

The web service can be tested using standard HTTP tools:

# Successful inquiry
curl -X GET \
  "https://cics-host:8443/api/v1/accounts/100045678901/balance" \
  -H "X-API-Key: sk_live_abc123def456ghi789jk" \
  -H "Accept: application/json"

# Account not found
curl -X GET \
  "https://cics-host:8443/api/v1/accounts/999999999999/balance" \
  -H "X-API-Key: sk_live_abc123def456ghi789jk" \
  -H "Accept: application/json"

# Missing API key (should return 401)
curl -X GET \
  "https://cics-host:8443/api/v1/accounts/100045678901/balance" \
  -H "Accept: application/json"

# Wrong method (should return 405)
curl -X POST \
  "https://cics-host:8443/api/v1/accounts/100045678901/balance" \
  -H "X-API-Key: sk_live_abc123def456ghi789jk" \
  -H "Accept: application/json"

Discussion Questions

  1. The API key validation compares against a hardcoded table of three keys. In production, how would you implement API key management? Consider storage (DB2 table, CICS resource, external OAuth provider), rotation, rate limiting, and revocation.

  2. The JSON is constructed manually using STRING statements. Compare this approach to using the CICS JSON assistant (DFHJS2LS utility) to auto-generate transformers from a COBOL copybook. What are the trade-offs in terms of development effort, flexibility, performance, and maintainability?

  3. The web bridge program and the core inquiry program are separate load modules. An alternative design would combine them into a single program. What are the advantages of the two-program approach? Consider reusability, independent deployment, testability, and the principle of separation of concerns.

  4. The current design handles only GET requests for balance inquiries. Design the URL structure and HTTP methods for a complete account API that supports balance inquiry (GET), transaction posting (POST), and account status update (PATCH). How would the channel/container structure change to accommodate these additional operations?

  5. The web service returns a single account's data. If a client needs to retrieve balances for multiple accounts (e.g., all accounts belonging to a customer), how would you design a batch inquiry endpoint? Consider the trade-offs between a single request with multiple account IDs versus multiple parallel requests, and how the channel/container pattern would need to evolve for the batch case.