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:
- Endpoint:
GET /api/v1/accounts/{accountId}/balance - Request: The account ID is passed as a path parameter in the URL
- Response: A JSON payload containing account number, account type, customer name, current balance, available balance, last activity date, and a status indicator
- 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
- Performance: Response time under 200 milliseconds for the 95th percentile
- Security: Validate the API key passed in the
X-API-KeyHTTP 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:
-
HTTP handling (ACBLWEB0) -- Parsing URLs, reading headers, formatting JSON, sending HTTP responses. This program knows about the web but not about databases.
-
Business logic (ACBLCORE) -- Querying the database and applying business rules. This program knows about accounts but not about HTTP.
-
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-REQUESTandINQ-RESPONSEare 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 TRIMto 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.99edit mask formats the number, andFUNCTION TRIMremoves 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
-
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.
-
The JSON is constructed manually using STRING statements. Compare this approach to using the CICS JSON assistant (
DFHJS2LSutility) to auto-generate transformers from a COBOL copybook. What are the trade-offs in terms of development effort, flexibility, performance, and maintainability? -
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.
-
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?
-
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.