Case Study 2: Interest Rate Table Lookup at Heartland Mortgage Corporation
Background
Heartland Mortgage Corporation originates approximately 15,000 residential mortgage loans per month across 12 states. Their loan origination system, running on an IBM z/OS mainframe, must calculate the interest rate for each loan application based on multiple factors: the loan amount, the borrower's credit score, the loan-to-value (LTV) ratio, and the loan term (15 or 30 years). These factors combine to determine the interest rate from a rate table that changes weekly as market conditions shift.
Previously, the rate-determination logic was embedded directly in the COBOL program as a cascade of nested IF statements -- over 800 lines of conditional logic that had to be modified every week when rates changed. A single misplaced period or incorrect threshold could result in thousands of loans being priced incorrectly, exposing the company to millions of dollars in losses.
Karen Yun, the lead developer for the loan origination system, was tasked with redesigning the rate-determination logic to be entirely table-driven. Instead of hard-coding rates in the program, the program would load the current rate table from a configuration file at runtime. When rates changed, only the file needed to be updated -- no program modifications, no recompilation, no testing of code changes.
The Problem
The rate table has three dimensions:
- Credit Score Tier (5 tiers): 800+, 750-799, 700-749, 650-699, below 650
- Loan Amount Range (6 ranges): Under $100K, $100K-$250K, $250K-$500K, $500K-$750K, $750K-$1M, Over $1M
- LTV Ratio Tier (4 tiers): 60% or less, 61-80%, 81-90%, 91-95%
Additionally, each combination has two rates: one for 15-year terms and one for 30-year terms.
The total number of rate entries is: 5 tiers x 6 ranges x 4 tiers x 2 terms = 240 rate entries.
The rate file is a sequential file with one record per entry. The number of entries can vary because Heartland occasionally adds or removes tiers. The program must handle a variable number of entries using OCCURS DEPENDING ON.
The lookup process must: 1. Load the rate table from the configuration file 2. Find the correct credit score tier for the borrower 3. Find the correct loan amount range 4. Find the correct LTV tier 5. Return the rate for the requested term 6. If any factor falls outside all defined ranges, return a "manual pricing required" indicator
The Solution
IDENTIFICATION DIVISION.
PROGRAM-ID. RATELKUP.
AUTHOR. KAREN YUN.
DATE-WRITTEN. 2024-06-10.
*================================================================
* PROGRAM: RATELKUP
* PURPOSE: Table-driven interest rate lookup for mortgage
* loan origination. Loads rate tiers from a
* configuration file using OCCURS DEPENDING ON
* and uses SEARCH ALL for efficient rate lookup.
* Demonstrates variable-length tables, multi-level
* SEARCH, and table-driven business logic.
*================================================================
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT RATE-CONFIG-FILE
ASSIGN TO "RATECFG"
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-RATE-STATUS.
SELECT LOAN-INPUT-FILE
ASSIGN TO "LOANIN"
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-LOAN-STATUS.
SELECT PRICED-OUTPUT-FILE
ASSIGN TO "LOANOUT"
ORGANIZATION IS SEQUENTIAL
FILE STATUS IS WS-OUT-STATUS.
DATA DIVISION.
FILE SECTION.
FD RATE-CONFIG-FILE
RECORDING MODE IS F
RECORD CONTAINS 80 CHARACTERS.
01 FS-RATE-RECORD.
05 FS-RT-RECORD-TYPE PIC X(1).
88 RT-CREDIT-TIER VALUE 'C'.
88 RT-AMOUNT-RANGE VALUE 'A'.
88 RT-LTV-TIER VALUE 'L'.
88 RT-RATE-ENTRY VALUE 'R'.
88 RT-HEADER VALUE 'H'.
05 FS-RT-DATA PIC X(79).
* Credit tier record layout (type 'C')
01 FS-CREDIT-TIER-REC.
05 FILLER PIC X(1).
05 FS-CT-TIER-NUM PIC 9(2).
05 FS-CT-MIN-SCORE PIC 9(3).
05 FS-CT-MAX-SCORE PIC 9(3).
05 FS-CT-DESCRIPTION PIC X(20).
05 FILLER PIC X(51).
* Amount range record layout (type 'A')
01 FS-AMOUNT-RANGE-REC.
05 FILLER PIC X(1).
05 FS-AR-RANGE-NUM PIC 9(2).
05 FS-AR-MIN-AMOUNT PIC 9(9).
05 FS-AR-MAX-AMOUNT PIC 9(9).
05 FS-AR-DESCRIPTION PIC X(20).
05 FILLER PIC X(39).
* LTV tier record layout (type 'L')
01 FS-LTV-TIER-REC.
05 FILLER PIC X(1).
05 FS-LT-TIER-NUM PIC 9(2).
05 FS-LT-MIN-LTV PIC 9(3).
05 FS-LT-MAX-LTV PIC 9(3).
05 FS-LT-DESCRIPTION PIC X(20).
05 FILLER PIC X(51).
* Rate entry record layout (type 'R')
01 FS-RATE-ENTRY-REC.
05 FILLER PIC X(1).
05 FS-RE-CREDIT-TIER PIC 9(2).
05 FS-RE-AMOUNT-RANGE PIC 9(2).
05 FS-RE-LTV-TIER PIC 9(2).
05 FS-RE-RATE-15YR PIC 9(2)V9(3).
05 FS-RE-RATE-30YR PIC 9(2)V9(3).
05 FILLER PIC X(66).
FD LOAN-INPUT-FILE
RECORDING MODE IS F
RECORD CONTAINS 100 CHARACTERS.
01 FS-LOAN-RECORD.
05 FS-LN-APP-NUMBER PIC 9(10).
05 FS-LN-BORROWER-NAME PIC X(30).
05 FS-LN-CREDIT-SCORE PIC 9(3).
05 FS-LN-LOAN-AMOUNT PIC 9(9).
05 FS-LN-PROPERTY-VALUE PIC 9(9).
05 FS-LN-TERM-YEARS PIC 9(2).
05 FILLER PIC X(37).
FD PRICED-OUTPUT-FILE
RECORDING MODE IS F
RECORD CONTAINS 120 CHARACTERS.
01 FS-PRICED-RECORD.
05 FS-PO-APP-NUMBER PIC 9(10).
05 FS-PO-BORROWER-NAME PIC X(30).
05 FS-PO-CREDIT-SCORE PIC 9(3).
05 FS-PO-LOAN-AMOUNT PIC 9(9).
05 FS-PO-LTV-RATIO PIC 9(3).
05 FS-PO-TERM-YEARS PIC 9(2).
05 FS-PO-INTEREST-RATE PIC 9(2)V9(3).
05 FS-PO-MONTHLY-PMT PIC 9(7)V99.
05 FS-PO-PRICING-STATUS PIC X(1).
88 PRICING-OK VALUE 'P'.
88 PRICING-MANUAL VALUE 'M'.
88 PRICING-DECLINED VALUE 'D'.
05 FS-PO-STATUS-DESC PIC X(30).
05 FILLER PIC X(18).
WORKING-STORAGE SECTION.
*----------------------------------------------------------------
* FILE STATUS
*----------------------------------------------------------------
01 WS-RATE-STATUS PIC X(2).
88 RATE-OK VALUE "00".
88 RATE-EOF VALUE "10".
01 WS-LOAN-STATUS PIC X(2).
88 LOAN-OK VALUE "00".
88 LOAN-EOF VALUE "10".
01 WS-OUT-STATUS PIC X(2).
88 OUT-OK VALUE "00".
*----------------------------------------------------------------
* CREDIT SCORE TIER TABLE (variable length)
*----------------------------------------------------------------
01 WS-CREDIT-TIER-COUNT PIC 9(2) VALUE ZERO.
01 WS-CREDIT-TIER-TABLE.
05 WS-CT-ENTRY
OCCURS 1 TO 10 TIMES
DEPENDING ON WS-CREDIT-TIER-COUNT
ASCENDING KEY IS WS-CT-MIN-SCORE
INDEXED BY WS-CT-IDX.
10 WS-CT-MIN-SCORE PIC 9(3).
10 WS-CT-MAX-SCORE PIC 9(3).
10 WS-CT-TIER-NUM PIC 9(2).
10 WS-CT-DESC PIC X(20).
*----------------------------------------------------------------
* LOAN AMOUNT RANGE TABLE (variable length)
*----------------------------------------------------------------
01 WS-AMOUNT-RANGE-COUNT PIC 9(2) VALUE ZERO.
01 WS-AMOUNT-RANGE-TABLE.
05 WS-AR-ENTRY
OCCURS 1 TO 10 TIMES
DEPENDING ON WS-AMOUNT-RANGE-COUNT
ASCENDING KEY IS WS-AR-MIN-AMT
INDEXED BY WS-AR-IDX.
10 WS-AR-MIN-AMT PIC 9(9).
10 WS-AR-MAX-AMT PIC 9(9).
10 WS-AR-RANGE-NUM PIC 9(2).
10 WS-AR-DESC PIC X(20).
*----------------------------------------------------------------
* LTV TIER TABLE (variable length)
*----------------------------------------------------------------
01 WS-LTV-TIER-COUNT PIC 9(2) VALUE ZERO.
01 WS-LTV-TIER-TABLE.
05 WS-LT-ENTRY
OCCURS 1 TO 10 TIMES
DEPENDING ON WS-LTV-TIER-COUNT
ASCENDING KEY IS WS-LT-MIN-LTV
INDEXED BY WS-LT-IDX.
10 WS-LT-MIN-LTV PIC 9(3).
10 WS-LT-MAX-LTV PIC 9(3).
10 WS-LT-TIER-NUM PIC 9(2).
10 WS-LT-DESC PIC X(20).
*----------------------------------------------------------------
* RATE LOOKUP TABLE (variable length)
* Composite key: credit tier + amount range + LTV tier
* Stored as a flat table with a composite sort key
* for SEARCH ALL.
*----------------------------------------------------------------
01 WS-RATE-ENTRY-COUNT PIC 9(4) VALUE ZERO.
01 WS-RATE-TABLE.
05 WS-RATE-ENTRY
OCCURS 1 TO 500 TIMES
DEPENDING ON WS-RATE-ENTRY-COUNT
ASCENDING KEY IS WS-RE-COMPOSITE-KEY
INDEXED BY WS-RE-IDX.
10 WS-RE-COMPOSITE-KEY.
15 WS-RE-CT-NUM PIC 9(2).
15 WS-RE-AR-NUM PIC 9(2).
15 WS-RE-LT-NUM PIC 9(2).
10 WS-RE-RATE-15 PIC 9(2)V9(3).
10 WS-RE-RATE-30 PIC 9(2)V9(3).
*----------------------------------------------------------------
* LOAN PROCESSING WORK FIELDS
*----------------------------------------------------------------
01 WS-CALC-LTV PIC 9(3).
01 WS-FOUND-CT-NUM PIC 9(2).
01 WS-FOUND-AR-NUM PIC 9(2).
01 WS-FOUND-LT-NUM PIC 9(2).
01 WS-LOOKUP-KEY.
05 WS-LK-CT-NUM PIC 9(2).
05 WS-LK-AR-NUM PIC 9(2).
05 WS-LK-LT-NUM PIC 9(2).
01 WS-SELECTED-RATE PIC 9(2)V9(3).
01 WS-TIER-FOUND PIC X(1).
88 TIER-FOUND VALUE 'Y'.
88 TIER-NOT-FOUND VALUE 'N'.
*----------------------------------------------------------------
* MONTHLY PAYMENT CALCULATION FIELDS
*----------------------------------------------------------------
01 WS-MONTHLY-RATE PIC 9V9(8) COMP-3.
01 WS-NUM-PAYMENTS PIC 9(4) COMP.
01 WS-PMT-FACTOR PIC 9(4)V9(8) COMP-3.
01 WS-TEMP-POWER PIC 9(4)V9(8) COMP-3.
*----------------------------------------------------------------
* COUNTERS AND STATISTICS
*----------------------------------------------------------------
01 WS-COUNTERS.
05 WS-LOANS-READ PIC S9(7) COMP-3 VALUE 0.
05 WS-LOANS-PRICED PIC S9(7) COMP-3 VALUE 0.
05 WS-LOANS-MANUAL PIC S9(7) COMP-3 VALUE 0.
05 WS-LOANS-DECLINED PIC S9(7) COMP-3 VALUE 0.
01 WS-DISP-COUNT PIC Z,ZZZ,ZZ9.
01 WS-DISP-RATE PIC Z9.999.
PROCEDURE DIVISION.
0000-MAIN-CONTROL.
PERFORM 1000-INITIALIZE
PERFORM 2000-LOAD-RATE-TABLE
PERFORM 3000-PROCESS-LOANS
UNTIL LOAN-EOF
PERFORM 8000-DISPLAY-STATISTICS
PERFORM 9000-FINALIZE
STOP RUN
.
1000-INITIALIZE.
DISPLAY "============================================="
DISPLAY " HEARTLAND MORTGAGE CORPORATION"
DISPLAY " INTEREST RATE TABLE LOOKUP SYSTEM"
DISPLAY "============================================="
OPEN INPUT RATE-CONFIG-FILE
INPUT LOAN-INPUT-FILE
OUTPUT PRICED-OUTPUT-FILE
IF NOT RATE-OK
DISPLAY "FATAL: Cannot open rate config file. "
"Status: " WS-RATE-STATUS
STOP RUN
END-IF
IF NOT LOAN-OK
DISPLAY "FATAL: Cannot open loan input file. "
"Status: " WS-LOAN-STATUS
STOP RUN
END-IF
.
2000-LOAD-RATE-TABLE.
* -------------------------------------------------------
* Read the rate configuration file and populate the
* tier tables and rate table. Each record type is
* identified by the first character.
* -------------------------------------------------------
DISPLAY " Loading rate configuration..."
MOVE ZERO TO WS-CREDIT-TIER-COUNT
MOVE ZERO TO WS-AMOUNT-RANGE-COUNT
MOVE ZERO TO WS-LTV-TIER-COUNT
MOVE ZERO TO WS-RATE-ENTRY-COUNT
READ RATE-CONFIG-FILE
AT END SET RATE-EOF TO TRUE
END-READ
PERFORM UNTIL RATE-EOF
EVALUATE TRUE
WHEN RT-CREDIT-TIER
PERFORM 2100-LOAD-CREDIT-TIER
WHEN RT-AMOUNT-RANGE
PERFORM 2200-LOAD-AMOUNT-RANGE
WHEN RT-LTV-TIER
PERFORM 2300-LOAD-LTV-TIER
WHEN RT-RATE-ENTRY
PERFORM 2400-LOAD-RATE-ENTRY
WHEN RT-HEADER
CONTINUE
WHEN OTHER
DISPLAY "WARNING: Unknown record type: "
FS-RT-RECORD-TYPE
END-EVALUATE
READ RATE-CONFIG-FILE
AT END SET RATE-EOF TO TRUE
END-READ
END-PERFORM
CLOSE RATE-CONFIG-FILE
DISPLAY " Credit score tiers loaded: "
WS-CREDIT-TIER-COUNT
DISPLAY " Amount ranges loaded: "
WS-AMOUNT-RANGE-COUNT
DISPLAY " LTV tiers loaded: "
WS-LTV-TIER-COUNT
DISPLAY " Rate entries loaded: "
WS-RATE-ENTRY-COUNT
DISPLAY " "
.
2100-LOAD-CREDIT-TIER.
MOVE FS-RATE-RECORD TO FS-CREDIT-TIER-REC
ADD 1 TO WS-CREDIT-TIER-COUNT
IF WS-CREDIT-TIER-COUNT > 10
DISPLAY "FATAL: Credit tier table overflow"
STOP RUN
END-IF
MOVE FS-CT-MIN-SCORE TO
WS-CT-MIN-SCORE(WS-CREDIT-TIER-COUNT)
MOVE FS-CT-MAX-SCORE TO
WS-CT-MAX-SCORE(WS-CREDIT-TIER-COUNT)
MOVE FS-CT-TIER-NUM TO
WS-CT-TIER-NUM(WS-CREDIT-TIER-COUNT)
MOVE FS-CT-DESCRIPTION TO
WS-CT-DESC(WS-CREDIT-TIER-COUNT)
.
2200-LOAD-AMOUNT-RANGE.
MOVE FS-RATE-RECORD TO FS-AMOUNT-RANGE-REC
ADD 1 TO WS-AMOUNT-RANGE-COUNT
IF WS-AMOUNT-RANGE-COUNT > 10
DISPLAY "FATAL: Amount range table overflow"
STOP RUN
END-IF
MOVE FS-AR-MIN-AMOUNT TO
WS-AR-MIN-AMT(WS-AMOUNT-RANGE-COUNT)
MOVE FS-AR-MAX-AMOUNT TO
WS-AR-MAX-AMT(WS-AMOUNT-RANGE-COUNT)
MOVE FS-AR-RANGE-NUM TO
WS-AR-RANGE-NUM(WS-AMOUNT-RANGE-COUNT)
MOVE FS-AR-DESCRIPTION TO
WS-AR-DESC(WS-AMOUNT-RANGE-COUNT)
.
2300-LOAD-LTV-TIER.
MOVE FS-RATE-RECORD TO FS-LTV-TIER-REC
ADD 1 TO WS-LTV-TIER-COUNT
IF WS-LTV-TIER-COUNT > 10
DISPLAY "FATAL: LTV tier table overflow"
STOP RUN
END-IF
MOVE FS-LT-TIER-NUM TO
WS-LT-TIER-NUM(WS-LTV-TIER-COUNT)
MOVE FS-LT-MIN-LTV TO
WS-LT-MIN-LTV(WS-LTV-TIER-COUNT)
MOVE FS-LT-MAX-LTV TO
WS-LT-MAX-LTV(WS-LTV-TIER-COUNT)
MOVE FS-LT-DESCRIPTION TO
WS-LT-DESC(WS-LTV-TIER-COUNT)
.
2400-LOAD-RATE-ENTRY.
MOVE FS-RATE-RECORD TO FS-RATE-ENTRY-REC
ADD 1 TO WS-RATE-ENTRY-COUNT
IF WS-RATE-ENTRY-COUNT > 500
DISPLAY "FATAL: Rate table overflow"
STOP RUN
END-IF
MOVE FS-RE-CREDIT-TIER TO
WS-RE-CT-NUM(WS-RATE-ENTRY-COUNT)
MOVE FS-RE-AMOUNT-RANGE TO
WS-RE-AR-NUM(WS-RATE-ENTRY-COUNT)
MOVE FS-RE-LTV-TIER TO
WS-RE-LT-NUM(WS-RATE-ENTRY-COUNT)
MOVE FS-RE-RATE-15YR TO
WS-RE-RATE-15(WS-RATE-ENTRY-COUNT)
MOVE FS-RE-RATE-30YR TO
WS-RE-RATE-30(WS-RATE-ENTRY-COUNT)
.
3000-PROCESS-LOANS.
* -------------------------------------------------------
* Read each loan application, determine the tier
* numbers, look up the rate, calculate the monthly
* payment, and write the priced output record.
* -------------------------------------------------------
READ LOAN-INPUT-FILE
AT END SET LOAN-EOF TO TRUE
END-READ
PERFORM UNTIL LOAN-EOF
ADD 1 TO WS-LOANS-READ
INITIALIZE FS-PRICED-RECORD
MOVE FS-LN-APP-NUMBER TO FS-PO-APP-NUMBER
MOVE FS-LN-BORROWER-NAME TO FS-PO-BORROWER-NAME
MOVE FS-LN-CREDIT-SCORE TO FS-PO-CREDIT-SCORE
MOVE FS-LN-LOAN-AMOUNT TO FS-PO-LOAN-AMOUNT
MOVE FS-LN-TERM-YEARS TO FS-PO-TERM-YEARS
* Calculate LTV ratio
IF FS-LN-PROPERTY-VALUE > ZERO
COMPUTE WS-CALC-LTV =
(FS-LN-LOAN-AMOUNT /
FS-LN-PROPERTY-VALUE) * 100
MOVE WS-CALC-LTV TO FS-PO-LTV-RATIO
ELSE
MOVE 999 TO WS-CALC-LTV
MOVE 999 TO FS-PO-LTV-RATIO
END-IF
* Find credit score tier
PERFORM 3100-FIND-CREDIT-TIER
IF TIER-NOT-FOUND
SET PRICING-DECLINED TO TRUE
MOVE "CREDIT SCORE OUT OF RANGE"
TO FS-PO-STATUS-DESC
ADD 1 TO WS-LOANS-DECLINED
PERFORM 3500-WRITE-OUTPUT
READ LOAN-INPUT-FILE
AT END SET LOAN-EOF TO TRUE
END-READ
EXIT PERFORM CYCLE
END-IF
* Find loan amount range
PERFORM 3200-FIND-AMOUNT-RANGE
IF TIER-NOT-FOUND
SET PRICING-MANUAL TO TRUE
MOVE "AMOUNT OUTSIDE STANDARD RANGE"
TO FS-PO-STATUS-DESC
ADD 1 TO WS-LOANS-MANUAL
PERFORM 3500-WRITE-OUTPUT
READ LOAN-INPUT-FILE
AT END SET LOAN-EOF TO TRUE
END-READ
EXIT PERFORM CYCLE
END-IF
* Find LTV tier
PERFORM 3300-FIND-LTV-TIER
IF TIER-NOT-FOUND
SET PRICING-MANUAL TO TRUE
MOVE "LTV EXCEEDS MAXIMUM"
TO FS-PO-STATUS-DESC
ADD 1 TO WS-LOANS-MANUAL
PERFORM 3500-WRITE-OUTPUT
READ LOAN-INPUT-FILE
AT END SET LOAN-EOF TO TRUE
END-READ
EXIT PERFORM CYCLE
END-IF
* Look up the rate using composite key
PERFORM 3400-LOOKUP-RATE
PERFORM 3500-WRITE-OUTPUT
READ LOAN-INPUT-FILE
AT END SET LOAN-EOF TO TRUE
END-READ
END-PERFORM
.
3100-FIND-CREDIT-TIER.
* -------------------------------------------------------
* Sequential search through credit tiers to find the
* range containing the borrower's score.
* (Cannot use SEARCH ALL here because we need range
* matching, not exact key matching.)
* -------------------------------------------------------
SET TIER-NOT-FOUND TO TRUE
SET WS-CT-IDX TO 1
SEARCH WS-CT-ENTRY
AT END
SET TIER-NOT-FOUND TO TRUE
WHEN FS-LN-CREDIT-SCORE >=
WS-CT-MIN-SCORE(WS-CT-IDX)
AND FS-LN-CREDIT-SCORE <=
WS-CT-MAX-SCORE(WS-CT-IDX)
SET TIER-FOUND TO TRUE
MOVE WS-CT-TIER-NUM(WS-CT-IDX)
TO WS-FOUND-CT-NUM
END-SEARCH
.
3200-FIND-AMOUNT-RANGE.
* Sequential search for the amount range
SET TIER-NOT-FOUND TO TRUE
SET WS-AR-IDX TO 1
SEARCH WS-AR-ENTRY
AT END
SET TIER-NOT-FOUND TO TRUE
WHEN FS-LN-LOAN-AMOUNT >=
WS-AR-MIN-AMT(WS-AR-IDX)
AND FS-LN-LOAN-AMOUNT <=
WS-AR-MAX-AMT(WS-AR-IDX)
SET TIER-FOUND TO TRUE
MOVE WS-AR-RANGE-NUM(WS-AR-IDX)
TO WS-FOUND-AR-NUM
END-SEARCH
.
3300-FIND-LTV-TIER.
* Sequential search for the LTV tier
SET TIER-NOT-FOUND TO TRUE
SET WS-LT-IDX TO 1
SEARCH WS-LT-ENTRY
AT END
SET TIER-NOT-FOUND TO TRUE
WHEN WS-CALC-LTV >=
WS-LT-MIN-LTV(WS-LT-IDX)
AND WS-CALC-LTV <=
WS-LT-MAX-LTV(WS-LT-IDX)
SET TIER-FOUND TO TRUE
MOVE WS-LT-TIER-NUM(WS-LT-IDX)
TO WS-FOUND-LT-NUM
END-SEARCH
.
3400-LOOKUP-RATE.
* -------------------------------------------------------
* Build the composite key and use SEARCH ALL (binary
* search) to find the matching rate entry.
* -------------------------------------------------------
MOVE WS-FOUND-CT-NUM TO WS-LK-CT-NUM
MOVE WS-FOUND-AR-NUM TO WS-LK-AR-NUM
MOVE WS-FOUND-LT-NUM TO WS-LK-LT-NUM
SET WS-RE-IDX TO 1
SEARCH ALL WS-RATE-ENTRY
AT END
SET PRICING-MANUAL TO TRUE
MOVE "RATE NOT IN TABLE"
TO FS-PO-STATUS-DESC
ADD 1 TO WS-LOANS-MANUAL
WHEN WS-RE-COMPOSITE-KEY(WS-RE-IDX) =
WS-LOOKUP-KEY
* Found the rate entry
IF FS-LN-TERM-YEARS = 15
MOVE WS-RE-RATE-15(WS-RE-IDX)
TO WS-SELECTED-RATE
ELSE
MOVE WS-RE-RATE-30(WS-RE-IDX)
TO WS-SELECTED-RATE
END-IF
MOVE WS-SELECTED-RATE
TO FS-PO-INTEREST-RATE
SET PRICING-OK TO TRUE
MOVE "STANDARD PRICING"
TO FS-PO-STATUS-DESC
ADD 1 TO WS-LOANS-PRICED
* Calculate monthly payment
PERFORM 3410-CALCULATE-PAYMENT
END-SEARCH
.
3410-CALCULATE-PAYMENT.
* -------------------------------------------------------
* Monthly payment = P * [r(1+r)^n] / [(1+r)^n - 1]
* where P = principal, r = monthly rate, n = payments
* -------------------------------------------------------
COMPUTE WS-MONTHLY-RATE =
WS-SELECTED-RATE / 100 / 12
COMPUTE WS-NUM-PAYMENTS =
FS-LN-TERM-YEARS * 12
* Calculate (1 + r)^n using COMPUTE with **
COMPUTE WS-TEMP-POWER =
(1 + WS-MONTHLY-RATE) ** WS-NUM-PAYMENTS
* Calculate monthly payment
IF WS-TEMP-POWER > 1
COMPUTE FS-PO-MONTHLY-PMT ROUNDED =
FS-LN-LOAN-AMOUNT *
(WS-MONTHLY-RATE * WS-TEMP-POWER) /
(WS-TEMP-POWER - 1)
ELSE
COMPUTE FS-PO-MONTHLY-PMT ROUNDED =
FS-LN-LOAN-AMOUNT / WS-NUM-PAYMENTS
END-IF
.
3500-WRITE-OUTPUT.
WRITE FS-PRICED-RECORD
IF NOT OUT-OK
DISPLAY "ERROR: Write failed. Status: "
WS-OUT-STATUS
END-IF
.
8000-DISPLAY-STATISTICS.
DISPLAY " "
DISPLAY "============================================="
DISPLAY " RATE LOOKUP STATISTICS"
DISPLAY "============================================="
MOVE WS-LOANS-READ TO WS-DISP-COUNT
DISPLAY " Applications processed: " WS-DISP-COUNT
MOVE WS-LOANS-PRICED TO WS-DISP-COUNT
DISPLAY " Standard pricing: " WS-DISP-COUNT
MOVE WS-LOANS-MANUAL TO WS-DISP-COUNT
DISPLAY " Manual pricing required: " WS-DISP-COUNT
MOVE WS-LOANS-DECLINED TO WS-DISP-COUNT
DISPLAY " Declined (out of range): " WS-DISP-COUNT
DISPLAY "============================================="
.
9000-FINALIZE.
CLOSE LOAN-INPUT-FILE
PRICED-OUTPUT-FILE
DISPLAY " "
DISPLAY "Processing complete."
.
Solution Walkthrough
OCCURS DEPENDING ON for Variable-Length Tables
Each tier table uses OCCURS DEPENDING ON to define a variable-length table:
05 WS-CT-ENTRY
OCCURS 1 TO 10 TIMES
DEPENDING ON WS-CREDIT-TIER-COUNT
ASCENDING KEY IS WS-CT-MIN-SCORE
INDEXED BY WS-CT-IDX.
This defines a table that can hold between 1 and 10 entries. The actual number of entries is controlled by WS-CREDIT-TIER-COUNT, which is incremented as records are loaded from the configuration file. COBOL uses this count to determine the upper bound for SEARCH operations and to control how much of the table is considered "active."
The rate table itself uses OCCURS 1 TO 500 TIMES DEPENDING ON WS-RATE-ENTRY-COUNT, which is large enough to accommodate any reasonable combination of tiers. With 5 credit tiers x 6 amount ranges x 4 LTV tiers x 1 record per combination = 120 entries, 500 provides comfortable headroom.
Two-Level Search: SEARCH for Ranges, SEARCH ALL for Exact Keys
The tier lookup uses sequential SEARCH (not SEARCH ALL) because it needs range matching -- finding the tier where the value falls between a minimum and a maximum:
SEARCH WS-CT-ENTRY
WHEN FS-LN-CREDIT-SCORE >=
WS-CT-MIN-SCORE(WS-CT-IDX)
AND FS-LN-CREDIT-SCORE <=
WS-CT-MAX-SCORE(WS-CT-IDX)
SET TIER-FOUND TO TRUE
END-SEARCH
SEARCH ALL requires an exact equality comparison (=). Range matching with >= and <= requires the sequential SEARCH. This is a critical distinction that many COBOL programmers overlook: SEARCH ALL is faster but only works for exact key matches.
Once the three tier numbers are determined, the rate lookup uses SEARCH ALL with a composite key:
SEARCH ALL WS-RATE-ENTRY
WHEN WS-RE-COMPOSITE-KEY(WS-RE-IDX) =
WS-LOOKUP-KEY
The composite key is a group item containing three tier numbers concatenated together. Because the rate table is loaded in ascending order of this composite key, SEARCH ALL performs a binary search that finds the matching rate in at most 8 comparisons (log2(240) = 7.9) instead of scanning up to 240 entries sequentially.
Composite Key Design
The composite key is structured as a group item:
10 WS-RE-COMPOSITE-KEY.
15 WS-RE-CT-NUM PIC 9(2).
15 WS-RE-AR-NUM PIC 9(2).
15 WS-RE-LT-NUM PIC 9(2).
When COBOL compares WS-RE-COMPOSITE-KEY to WS-LOOKUP-KEY, it treats both as 6-byte alphanumeric fields and compares them byte-by-byte. Because the tier numbers are zero-padded two-digit values, this byte-by-byte comparison produces the same result as comparing each component individually. This technique -- using a group item as a composite search key -- is a common production COBOL pattern.
Monthly Payment Calculation
The payment calculation uses COBOL's exponentiation operator (**) to compute compound interest:
COMPUTE WS-TEMP-POWER =
(1 + WS-MONTHLY-RATE) ** WS-NUM-PAYMENTS
This computes (1 + r)^n where r is the monthly rate and n is the number of payments. The standard amortization formula is then applied to determine the monthly payment. The guard clause IF WS-TEMP-POWER > 1 prevents division by zero in the edge case where the rate is exactly zero (which would make the power equal to 1, causing a zero denominator).
Lessons Learned
1. Table-Driven Logic Eliminates Weekly Code Changes
The previous 800-line IF/ELSE structure required weekly modifications. The table-driven approach moves all rate data to an external file that business analysts can update without programmer involvement. This reduces change risk from "code modification" to "data file update."
2. SEARCH ALL Requires Exact Equality -- Use SEARCH for Ranges
The choice between SEARCH and SEARCH ALL depends on the type of matching. Range lookups (is the value between min and max?) require SEARCH. Exact key lookups require SEARCH ALL. Many programs need both, as this case study demonstrates.
3. OCCURS DEPENDING ON Adds Flexibility but Requires Overflow Protection
Variable-length tables adapt to changing business requirements (adding or removing tiers) without recompilation. However, the program must check for table overflow -- attempting to load the 11th entry into a table defined for 10 would corrupt memory silently. Every load paragraph in this program includes an overflow check.
4. Composite Keys Enable Multi-Dimensional Binary Search
By concatenating multiple key components into a single group item and sorting by that group, a multi-dimensional lookup can be performed with a single SEARCH ALL. This is far more efficient than nested SEARCH operations.
5. Separating Tier Resolution from Rate Lookup Improves Maintainability
The three-step process (find credit tier, find amount range, find LTV tier, then look up rate) means each step can be modified independently. If a new dimension is added (such as a geographic region tier), only a new search paragraph and a wider composite key are needed.
Discussion Questions
-
The credit score tier table is searched sequentially because SEARCH ALL cannot handle range matching. With only 5 tiers, this is not a performance concern. At what number of tiers would the sequential search become a bottleneck? How could you redesign the lookup for better performance with many tiers?
-
The composite key for SEARCH ALL concatenates three two-digit numbers into a six-character string. What would happen if a tier number exceeded 99 (required three digits)? How would you handle this without breaking existing data?
-
The program loads the entire rate table into memory before processing any loans. An alternative would be to read rates from a VSAM file for each loan. Compare the memory usage, I/O performance, and code complexity of these two approaches for 15,000 loans per month.
-
The monthly payment calculation uses the
**(exponentiation) operator. On older COBOL compilers, this operator may not support non-integer exponents or may produce imprecise results with large exponents. How would you verify the calculation's accuracy, and what alternative approach could you use? -
OCCURS DEPENDING ON defines the table's active size but allocates memory for the maximum size (500 entries for the rate table). What is the memory cost of this allocation? In what environments would this be a concern?
-
The configuration file uses a record-type indicator (first character) to distinguish between tier records and rate records. An alternative design would use separate files for each table. What are the advantages and disadvantages of each approach for operational management?
-
How would you add a "rate adjustment" feature where certain borrower characteristics (such as being an existing customer or setting up autopay) reduce the base rate by a fixed amount? Would this be better implemented as additional table columns or as post-lookup logic?