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:

  1. Credit Score Tier (5 tiers): 800+, 750-799, 700-749, 650-699, below 650
  2. Loan Amount Range (6 ranges): Under $100K, $100K-$250K, $250K-$500K, $500K-$750K, $750K-$1M, Over $1M
  3. 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.

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

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

  2. 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?

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

  4. 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?

  5. 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?

  6. 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?

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