Case Study 2: MedClaim's Nested Validation Routines in CLM-INTAKE

Background

MedClaim's claim intake program (CLM-INTAKE) is the system's front door — it accepts incoming claims from electronic data interchange (EDI) feeds, validates them against business rules, and either accepts them for adjudication or rejects them with detailed error messages.

When James Okafor redesigned CLM-INTAKE in 2021, he faced a design question: the program needed five distinct validation stages (member ID, provider ID, charge amounts, diagnosis codes, procedure codes), each with its own business rules. Should these be external subprograms or nested programs?

"The validation rules for intake are specific to intake," James reasoned. "The adjudication engine has its own validation rules. Provider maintenance has its own. These validators share a purpose — validate a claim — but they aren't reusable outside of CLM-INTAKE. That pointed me toward nested programs."

The Design Challenge

The validators had two competing needs:

  1. Independence: Each validator should have its own working storage, its own logic, and no accidental interference with other validators.

  2. Shared error collection: All validators need to accumulate errors in a single error table so the system can report all validation failures (not just the first one) in the rejection notice sent back to the submitter.

With external subprograms, error accumulation would require either passing a large error table as a parameter to every validator or creating a separate error-logging subprogram. With nested programs and GLOBAL, the error table could be directly shared.

Architecture

CLM-INTAKE (outer)
├── VAL-MEMBER (COMMON)      — Validates member ID
├── VAL-PROVIDER (COMMON)    — Validates provider ID
├── VAL-AMOUNTS (COMMON)     — Validates charge amounts
├── VAL-CODES (COMMON)       — Validates diagnosis/procedure codes
└── BUILD-ERRORS (COMMON)    — Accumulates errors in GLOBAL table

GLOBAL Items

James defined three GLOBAL items:

01  WS-ERROR-TABLE IS GLOBAL.
    05  WS-ERROR-COUNT    PIC 9(2) VALUE 0.
    05  WS-ERROR-ENTRY    OCCURS 20 TIMES.
        10  WS-ERR-FIELD  PIC X(20).
        10  WS-ERR-MSG    PIC X(50).
        10  WS-ERR-SEV    PIC 9(2).

01  WS-PROCESSING-DATE PIC 9(8) IS GLOBAL.
01  WS-BATCH-ID        PIC X(12) IS GLOBAL.

The BUILD-ERRORS Pattern

Rather than having each validator directly modify the GLOBAL error table (which would scatter error-accumulation logic across five programs), James centralized it in BUILD-ERRORS:

PROGRAM-ID. BUILD-ERRORS IS COMMON.

All validators call BUILD-ERRORS to add an error. BUILD-ERRORS checks the count, adds the entry if there is room, and handles the overflow case. This centralization means the error-accumulation logic exists in exactly one place.

Why All Validators Are COMMON

Every validator is marked COMMON for one reason: they all need to call BUILD-ERRORS, which is a sibling. Without COMMON, only the outer program could call BUILD-ERRORS. With COMMON on all programs, any sibling can call any other sibling.

Sarah Kim initially questioned this: "If everything is COMMON, doesn't that defeat the purpose? Any program can call any other program." James responded: "It does reduce the access control, but the alternative — passing BUILD-ERRORS as some kind of callback — is not idiomatic COBOL. The pragmatic choice is to make them all COMMON and rely on discipline and documentation."

Data Flow Diagram

CLM-INTAKE reads claim
    │
    ├──► CALL VAL-MEMBER
    │       └── CALL BUILD-ERRORS (if errors found)
    │           └── writes to GLOBAL WS-ERROR-TABLE
    │
    ├──► CALL 'DTEVALID' (external — shared utility)
    │       └── CLM-INTAKE calls BUILD-ERRORS with result
    │
    ├──► CALL VAL-PROVIDER
    │       └── CALL BUILD-ERRORS (if errors found)
    │
    ├──► CALL VAL-AMOUNTS
    │       └── CALL BUILD-ERRORS (if errors found)
    │
    └──► CALL VAL-CODES
            └── CALL BUILD-ERRORS (if errors found)

    CLM-INTAKE reads GLOBAL WS-ERROR-COUNT
    └── If > 0: reject claim, report all errors
    └── If 0: accept claim, route to adjudication

Results After Six Months

Metric Before (monolithic validation) After (nested validators)
Validation code organization Single 800-line paragraph 5 programs, ~100 lines each
Errors reported per rejection 1 (first error only) All errors (up to 20)
Provider rejection callbacks 3.2 per claim (avg) 1.1 per claim (avg)
Time to add new validation rule 2 days 4 hours
Regression test scope Entire program Individual validator

The reduction in provider callbacks was the most significant business impact. Previously, a claim with three errors would be rejected three separate times — once for each error, as the submitter would fix one error and resubmit, only to hit the next one. Now, all errors are reported at once, and the submitter can fix them all before resubmitting.

The Error Overflow Decision

Sarah Kim raised a concern during design review: "What happens when a claim has more than 20 errors?"

James's solution was pragmatic:

       PROCEDURE DIVISION USING LS-FIELD LS-MSG LS-SEV.
           IF WS-ERROR-COUNT < 20
               ADD 1 TO WS-ERROR-COUNT
               MOVE LS-FIELD TO WS-ERR-FIELD(WS-ERROR-COUNT)
               MOVE LS-MSG   TO WS-ERR-MSG(WS-ERROR-COUNT)
               MOVE LS-SEV   TO WS-ERR-SEV(WS-ERROR-COUNT)
           ELSE
      *        Log overflow to system log, but don't lose
      *        earlier errors. The first 20 are the most
      *        important anyway.
               DISPLAY 'WARN: Error table overflow for '
                       'claim in batch ' WS-BATCH-ID
           END-IF
           GOBACK.

"In practice, a claim with more than 20 errors is almost certainly garbled EDI data, not a real claim with fixable problems," James noted. "Twenty errors is more than enough for legitimate cases."

Challenges Encountered

Challenge 1: Testing GLOBAL Interactions

Unit testing individual validators required careful setup of the GLOBAL error table. Before each test, WS-ERROR-COUNT had to be reset to 0. This was not obvious to new team members who were accustomed to external subprograms where each module's state was independent.

Challenge 2: Order-Dependent Behavior

Because all validators share the GLOBAL error table, the order of validation calls matters. If VAL-MEMBER adds 15 errors and VAL-AMOUNTS adds 10, the error table overflows. Reordering the calls changes which errors are captured.

Challenge 3: Source File Size

CLM-INTAKE grew to 1,400 lines with the nested programs included. While manageable, James noted it was approaching the limit: "Beyond 1,500 lines, I'd consider splitting into external subprograms, even if they're not reused."

Lessons Learned

  1. GLOBAL is powerful but requires discipline: Shared mutable state is the source of many bugs. Use GLOBAL for infrastructure (error tables, files), not for business data.

  2. Centralize GLOBAL access when possible: BUILD-ERRORS is the single point of access to the error table. This makes the behavior predictable and testable.

  3. All-COMMON is sometimes the pragmatic choice: When sibling programs need to call a shared utility, marking everything COMMON is simpler than creating complex nesting hierarchies.

  4. Nested programs improve error reporting: The GLOBAL error table pattern enables comprehensive error accumulation that would be awkward with external subprograms.

  5. Watch the source file size: Nested programs consolidate code into one file. Set a limit (James uses 1,500 lines) and refactor to external subprograms when the limit is reached.

Discussion Questions

  1. James chose GLOBAL for the error table rather than passing it as a parameter. If he had passed it as a parameter, how would the CALL statements change? Would this be better or worse for readability?

  2. The BUILD-ERRORS pattern centralizes error accumulation. What would happen if each validator modified the GLOBAL error table directly instead of going through BUILD-ERRORS?

  3. If MedClaim adds a new validation stage — say, prior authorization verification — how would you integrate it into this structure? Would you add another nested program or make it external?

  4. The testing challenge mentions that GLOBAL state must be reset before each test. How would you automate this? Could you write a nested RESET-TEST-STATE program?

  5. Sarah Kim observed that nested programs improved onboarding time for new developers. Why might a single source file with related programs be easier to learn than multiple separate source files?