25 min read

> "Nested programs are like apartments in a building. Each has its own space, its own key, and its own privacy. But some facilities — the lobby, the laundry room, the mailboxes — are shared. Understanding which is which is the difference between a...

Chapter 24: Nested Programs

"Nested programs are like apartments in a building. Each has its own space, its own key, and its own privacy. But some facilities — the lobby, the laundry room, the mailboxes — are shared. Understanding which is which is the difference between a well-organized building and a chaotic one." — Priya Kapoor

In Chapters 22 and 23, you learned how to build modular COBOL systems using separately compiled external subprograms connected by CALL statements. That approach — the dominant one in production mainframe systems — requires each subprogram to be compiled, link-edited, and deployed as an independent unit.

COBOL offers an alternative: nested programs (also called contained programs). A nested program is defined entirely within another program's source code, between the outer program's IDENTIFICATION DIVISION and its END PROGRAM marker. Nested programs share a single compilation unit while maintaining their own data and procedure divisions.

This chapter explores when nested programs are the right choice, how to use them effectively, and when to prefer external subprograms instead.

24.1 What Are Nested Programs?

A nested program is a complete COBOL program defined inside another COBOL program. The containing program is called the outermost (or parent) program. The programs defined inside it are contained (or nested or child) programs.

Here is the simplest possible nested program structure:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OUTER-PROG.
      *    (Data and procedure divisions of OUTER-PROG)

           IDENTIFICATION DIVISION.
           PROGRAM-ID. INNER-PROG.
          *    (Data and procedure divisions of INNER-PROG)
           END PROGRAM INNER-PROG.

       END PROGRAM OUTER-PROG.

Key structural observations:

  1. INNER-PROG is physically located inside OUTER-PROG's source code.
  2. Both programs have their own IDENTIFICATION, DATA, and PROCEDURE DIVISIONS.
  3. END PROGRAM markers are mandatory — they define the boundaries of each program.
  4. OUTER-PROG can CALL INNER-PROG just like it would call any subprogram.
  5. The entire structure compiles as a single compilation unit.

A Complete Minimal Example

       IDENTIFICATION DIVISION.
       PROGRAM-ID. GREET-MAIN.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-NAME             PIC X(30) VALUE 'World'.
       01  WS-GREETING         PIC X(50).
       01  WS-RETURN-CODE      PIC S9(4) COMP.

       PROCEDURE DIVISION.
       MAIN-LOGIC.
           CALL 'BUILD-GREETING' USING WS-NAME
                                       WS-GREETING
                                       WS-RETURN-CODE

           DISPLAY WS-GREETING
           STOP RUN.

      *================================================================
      * Nested program: BUILD-GREETING
      * Constructs a greeting message from a name.
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. BUILD-GREETING.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TRIMMED-NAME     PIC X(30).
       01  WS-NAME-LEN         PIC 9(2).

       LINKAGE SECTION.
       01  LS-NAME             PIC X(30).
       01  LS-GREETING         PIC X(50).
       01  LS-RETURN-CODE      PIC S9(4) COMP.

       PROCEDURE DIVISION USING LS-NAME
                                LS-GREETING
                                LS-RETURN-CODE.
       BUILD-IT.
           MOVE 0 TO LS-RETURN-CODE
           MOVE SPACES TO LS-GREETING

           MOVE LS-NAME TO WS-TRIMMED-NAME
           INSPECT WS-TRIMMED-NAME TALLYING WS-NAME-LEN
               FOR CHARACTERS BEFORE INITIAL SPACES

           IF WS-NAME-LEN = 0
               MOVE 8 TO LS-RETURN-CODE
               GOBACK
           END-IF

           STRING 'Hello, '
                  WS-TRIMMED-NAME(1:WS-NAME-LEN)
                  '! Welcome to COBOL.'
               DELIMITED BY SIZE
               INTO LS-GREETING
           END-STRING

           GOBACK.

       END PROGRAM BUILD-GREETING.

       END PROGRAM GREET-MAIN.

When you compile this source file, you get one compilation unit containing two programs: GREET-MAIN and BUILD-GREETING. When GREET-MAIN calls BUILD-GREETING, the call resolves within the same load module — there is no dynamic search, no separate compilation, no separate deployment.

24.2 The Structure of Nested Programs

Nesting can go multiple levels deep, and a single outer program can contain multiple nested programs:

OUTER-PROG
├── NESTED-A
│   ├── DEEPLY-NESTED-A1
│   └── DEEPLY-NESTED-A2
├── NESTED-B
└── NESTED-C
    └── DEEPLY-NESTED-C1

In source code, this looks like:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OUTER-PROG.
       ...
           IDENTIFICATION DIVISION.
           PROGRAM-ID. NESTED-A.
           ...
               IDENTIFICATION DIVISION.
               PROGRAM-ID. DEEPLY-NESTED-A1.
               ...
               END PROGRAM DEEPLY-NESTED-A1.

               IDENTIFICATION DIVISION.
               PROGRAM-ID. DEEPLY-NESTED-A2.
               ...
               END PROGRAM DEEPLY-NESTED-A2.
           END PROGRAM NESTED-A.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. NESTED-B.
           ...
           END PROGRAM NESTED-B.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. NESTED-C.
           ...
               IDENTIFICATION DIVISION.
               PROGRAM-ID. DEEPLY-NESTED-C1.
               ...
               END PROGRAM DEEPLY-NESTED-C1.
           END PROGRAM NESTED-C.

       END PROGRAM OUTER-PROG.

⚠️ Important: The END PROGRAM markers must nest properly, like balanced parentheses. Every PROGRAM-ID must have a corresponding END PROGRAM, and they must be in the correct order (innermost closed first).

Who Can Call Whom?

The default calling rules for nested programs are:

  1. A program can call its directly contained programs (its immediate children).
  2. A program cannot call its siblings by default.
  3. A program cannot call its parent.
  4. A program cannot call a program nested inside a sibling.

In the structure above: - OUTER-PROG can call NESTED-A, NESTED-B, and NESTED-C - NESTED-A can call DEEPLY-NESTED-A1 and DEEPLY-NESTED-A2 - NESTED-A cannot call NESTED-B or NESTED-C (they are siblings) - DEEPLY-NESTED-A1 cannot call DEEPLY-NESTED-A2 (they are siblings) - NESTED-B cannot call DEEPLY-NESTED-A1 (nested inside a sibling)

These restrictions are relaxed by the COMMON attribute, which we will examine in Section 24.4.

24.3 Nesting Depth and Practical Limits

While COBOL allows arbitrary nesting depth, practical considerations limit how deep you should nest.

Maximum Nesting Depth

The COBOL standard does not specify a maximum nesting depth. IBM Enterprise COBOL supports at least 64 levels of nesting. GnuCOBOL similarly supports deep nesting. In practice, you will rarely need more than two or three levels:

Level 0: Outer program (the main driver)
Level 1: Directly contained programs (workers and utilities)
Level 2: Programs contained within Level 1 (rare, specialized)
Level 3+: Almost never needed

When Deep Nesting Makes Sense

Deep nesting is occasionally useful when a Level 1 program has its own internal utilities that should not be visible to other Level 1 programs:

       PROGRAM-ID. PAYROLL-SYSTEM.       (Level 0)

           PROGRAM-ID. CALC-GROSS.       (Level 1)
      *        Only CALC-GROSS can call these:
               PROGRAM-ID. OVERTIME-CALC.  (Level 2)
               END PROGRAM OVERTIME-CALC.
               PROGRAM-ID. SHIFT-DIFF.     (Level 2)
               END PROGRAM SHIFT-DIFF.
           END PROGRAM CALC-GROSS.

           PROGRAM-ID. CALC-NET.          (Level 1)
      *        CALC-NET cannot call OVERTIME-CALC or SHIFT-DIFF
      *        — they are hidden inside CALC-GROSS
           END PROGRAM CALC-NET.

       END PROGRAM PAYROLL-SYSTEM.

OVERTIME-CALC and SHIFT-DIFF are implementation details of CALC-GROSS. They are invisible to CALC-NET and PAYROLL-SYSTEM (unless marked COMMON, in which case they would be visible to CALC-GROSS's siblings, which is rarely the intent).

💡 Practitioner's Insight: "In twenty years of COBOL development, I've needed Level 2 nesting exactly twice. Both times it was for a complex report generator where the detail-line formatter had its own sub-formatters for currency, dates, and percentages. For everything else, one level of nesting is sufficient." — Maria Chen

24.4 Data Scope in Nested Programs

Each nested program has its own WORKING-STORAGE SECTION (and LOCAL-STORAGE, if used). By default, data items in one program are not visible to any other program — even programs nested inside it. Each program's data is private.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OUTER.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-OUTER-DATA      PIC X(20) VALUE 'Outer data'.
      *    WS-OUTER-DATA is visible ONLY within OUTER's
      *    PROCEDURE DIVISION. INNER cannot see it.

       PROCEDURE DIVISION.
           DISPLAY WS-OUTER-DATA
           CALL 'INNER'
           STOP RUN.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. INNER.

           DATA DIVISION.
           WORKING-STORAGE SECTION.
           01  WS-INNER-DATA  PIC X(20) VALUE 'Inner data'.
          *    WS-INNER-DATA is visible ONLY within INNER.
          *    OUTER cannot see it.

           PROCEDURE DIVISION.
               DISPLAY WS-INNER-DATA
      *        DISPLAY WS-OUTER-DATA    <- COMPILE ERROR!
               GOBACK.

           END PROGRAM INNER.

       END PROGRAM OUTER.

This privacy is a feature, not a limitation. It provides encapsulation — each program manages its own state without interference from other programs.

Sharing Data Between Nested Programs

There are two mechanisms for sharing data between nested programs:

  1. Parameter passing via CALL USING (just like external subprograms)
  2. The GLOBAL clause (unique to nested programs)

Parameter passing is the preferred approach for most cases. The GLOBAL clause is for specific situations where parameter passing is impractical, as we will see in Section 24.5.

24.4 The COMMON Attribute

The COMMON attribute, specified on the PROGRAM-ID paragraph, makes a nested program callable by its siblings (other programs at the same nesting level) — not just by its parent.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OUTER.

       PROCEDURE DIVISION.
           CALL 'PROC-A'
           CALL 'PROC-B'
           CALL 'UTILITY'
           STOP RUN.

      *    PROC-A can call UTILITY because UTILITY is COMMON
           IDENTIFICATION DIVISION.
           PROGRAM-ID. PROC-A.
           PROCEDURE DIVISION.
               DISPLAY 'In PROC-A'
               CALL 'UTILITY'
               GOBACK.
           END PROGRAM PROC-A.

      *    PROC-B can also call UTILITY
           IDENTIFICATION DIVISION.
           PROGRAM-ID. PROC-B.
           PROCEDURE DIVISION.
               DISPLAY 'In PROC-B'
               CALL 'UTILITY'
               GOBACK.
           END PROGRAM PROC-B.

      *    UTILITY is COMMON — callable by siblings
           IDENTIFICATION DIVISION.
           PROGRAM-ID. UTILITY IS COMMON.
           PROCEDURE DIVISION.
               DISPLAY 'In UTILITY (COMMON)'
               GOBACK.
           END PROGRAM UTILITY.

       END PROGRAM OUTER.

Without the COMMON attribute, PROC-A and PROC-B could not call UTILITY — only OUTER could. With COMMON, UTILITY becomes a shared resource available to all programs at the same nesting level.

COMMON Rules

  1. Only a directly contained program can be declared COMMON.
  2. A COMMON program is visible to its parent and all of its parent's directly contained programs (its siblings).
  3. A COMMON program is not visible to programs nested inside its siblings (nephews/nieces).
  4. The COMMON attribute affects visibility only — it does not change data sharing rules.

The COMMON Utility Pattern

The most common use of COMMON is for utility routines shared by sibling programs:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. ORDER-PROC.
      *================================================================
      * Order Processing — Outer Program
      * Contains processing programs and shared COMMON utilities.
      *================================================================

       PROCEDURE DIVISION.
           CALL 'VALIDATE-ORDER'
           CALL 'CALCULATE-TOTAL'
           CALL 'GENERATE-INVOICE'
           STOP RUN.

      *    --- Processing Programs ---
           IDENTIFICATION DIVISION.
           PROGRAM-ID. VALIDATE-ORDER.
           DATA DIVISION.
           WORKING-STORAGE SECTION.
           01  WS-RESULT        PIC X(20).
           01  WS-RC            PIC S9(4) COMP.
           PROCEDURE DIVISION.
               DISPLAY 'Validating order...'
      *        Can call FORMAT-DATE because it is COMMON
               CALL 'FORMAT-DATE' USING WS-RESULT WS-RC
               DISPLAY 'Formatted date: ' WS-RESULT
               GOBACK.
           END PROGRAM VALIDATE-ORDER.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. CALCULATE-TOTAL.
           DATA DIVISION.
           WORKING-STORAGE SECTION.
           01  WS-RESULT        PIC X(20).
           01  WS-RC            PIC S9(4) COMP.
           PROCEDURE DIVISION.
               DISPLAY 'Calculating total...'
      *        Can also call FORMAT-DATE
               CALL 'FORMAT-DATE' USING WS-RESULT WS-RC
               GOBACK.
           END PROGRAM CALCULATE-TOTAL.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. GENERATE-INVOICE.
           DATA DIVISION.
           WORKING-STORAGE SECTION.
           01  WS-RESULT        PIC X(20).
           01  WS-RC            PIC S9(4) COMP.
           PROCEDURE DIVISION.
               DISPLAY 'Generating invoice...'
               CALL 'FORMAT-DATE' USING WS-RESULT WS-RC
               GOBACK.
           END PROGRAM GENERATE-INVOICE.

      *    --- COMMON Utility Programs ---
           IDENTIFICATION DIVISION.
           PROGRAM-ID. FORMAT-DATE IS COMMON.
           DATA DIVISION.
           WORKING-STORAGE SECTION.
           01  WS-CURRENT-DATE  PIC X(21).
           LINKAGE SECTION.
           01  LS-FORMATTED     PIC X(20).
           01  LS-RC            PIC S9(4) COMP.
           PROCEDURE DIVISION USING LS-FORMATTED LS-RC.
               MOVE 0 TO LS-RC
               MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DATE
               STRING WS-CURRENT-DATE(5:2) '/'
                      WS-CURRENT-DATE(7:2) '/'
                      WS-CURRENT-DATE(1:4)
                   DELIMITED BY SIZE INTO LS-FORMATTED
               GOBACK.
           END PROGRAM FORMAT-DATE.

       END PROGRAM ORDER-PROC.

This pattern creates a self-contained module: ORDER-PROC contains its processing logic and its shared utilities, all in one compilation unit.

24.5 The GLOBAL Clause

The GLOBAL clause allows a data item defined in an outer program to be visible (accessible) to all programs contained within that outer program — without passing it as a parameter.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. OUTER.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-SHARED-COUNTER  PIC 9(5) VALUE 0 IS GLOBAL.
       01  WS-PRIVATE-DATA    PIC X(20) VALUE 'Private'.

       PROCEDURE DIVISION.
           CALL 'INNER-A'
           CALL 'INNER-B'
           DISPLAY 'Final counter: ' WS-SHARED-COUNTER
           STOP RUN.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. INNER-A.
           PROCEDURE DIVISION.
      *        Can access WS-SHARED-COUNTER because it's GLOBAL
               ADD 10 TO WS-SHARED-COUNTER
               DISPLAY 'INNER-A: counter = ' WS-SHARED-COUNTER
      *        CANNOT access WS-PRIVATE-DATA — it's not GLOBAL
               GOBACK.
           END PROGRAM INNER-A.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. INNER-B.
           PROCEDURE DIVISION.
               ADD 5 TO WS-SHARED-COUNTER
               DISPLAY 'INNER-B: counter = ' WS-SHARED-COUNTER
               GOBACK.
           END PROGRAM INNER-B.

       END PROGRAM OUTER.

Output:

INNER-A: counter = 00010
INNER-B: counter = 00015
Final counter: 00015

Both INNER-A and INNER-B can access and modify WS-SHARED-COUNTER because it was declared GLOBAL in the outer program.

GLOBAL Rules

  1. A GLOBAL item is visible to the program that defines it and all programs directly or indirectly contained within it.
  2. A GLOBAL item is not visible to the program's parent or siblings — only downward in the nesting hierarchy.
  3. If a contained program defines a data item with the same name as a GLOBAL item in its parent, the local definition takes precedence (it shadows the GLOBAL item).
  4. GLOBAL can be applied to: - Data items in WORKING-STORAGE - File definitions (FD) - File status items
  5. GLOBAL applies to the entire 01-level group and all its subordinates.

GLOBAL Files

One of the most useful applications of GLOBAL is sharing file access:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. REPORT-GEN.

       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT REPORT-FILE ASSIGN TO 'REPORT.OUT'
               FILE STATUS IS WS-FILE-STATUS.

       DATA DIVISION.
       FILE SECTION.
       FD  REPORT-FILE IS GLOBAL.
       01  REPORT-RECORD      PIC X(132).

       WORKING-STORAGE SECTION.
       01  WS-FILE-STATUS     PIC X(2) IS GLOBAL.
       01  WS-LINE-COUNT      PIC 9(4) IS GLOBAL VALUE 0.
       01  WS-PAGE-COUNT      PIC 9(4) IS GLOBAL VALUE 0.

       PROCEDURE DIVISION.
       MAIN-LOGIC.
           OPEN OUTPUT REPORT-FILE
           CALL 'PRINT-HEADER'
           CALL 'PRINT-DETAIL'
           CALL 'PRINT-FOOTER'
           CLOSE REPORT-FILE
           STOP RUN.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. PRINT-HEADER IS COMMON.
           PROCEDURE DIVISION.
      *        Can WRITE to REPORT-FILE because it's GLOBAL
               MOVE 'MONTHLY REPORT' TO REPORT-RECORD
               WRITE REPORT-RECORD AFTER ADVANCING PAGE
               ADD 1 TO WS-PAGE-COUNT
               MOVE 1 TO WS-LINE-COUNT
               GOBACK.
           END PROGRAM PRINT-HEADER.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. PRINT-DETAIL IS COMMON.
           PROCEDURE DIVISION.
      *        Can also write to the same file
               MOVE 'Detail line here...' TO REPORT-RECORD
               WRITE REPORT-RECORD AFTER ADVANCING 1 LINE
               ADD 1 TO WS-LINE-COUNT
               GOBACK.
           END PROGRAM PRINT-DETAIL.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. PRINT-FOOTER IS COMMON.
           PROCEDURE DIVISION.
               MOVE 'End of Report' TO REPORT-RECORD
               WRITE REPORT-RECORD AFTER ADVANCING 2 LINES
               ADD 1 TO WS-LINE-COUNT
               GOBACK.
           END PROGRAM PRINT-FOOTER.

       END PROGRAM REPORT-GEN.

In this example, the REPORT-FILE is opened once by the outer program and shared with all nested programs via GLOBAL. The nested programs can WRITE to the file without needing to receive a file handle as a parameter.

⚠️ Warning: GLOBAL creates implicit coupling between programs. If INNER-A modifies WS-SHARED-COUNTER, INNER-B sees the modified value — even though INNER-B did not receive it as a parameter. This implicit sharing can make programs harder to understand and debug, especially as the nesting structure grows complex.

When GLOBAL Is the Right Choice

Despite the coupling concerns, GLOBAL is clearly the right choice in several situations:

Situation 1: Shared output files. When multiple nested programs write to the same report or log file, GLOBAL on the FD eliminates the need to pass file handles or status codes. The outer program opens and closes the file; nested programs write to it directly.

Situation 2: Shared counters and accumulators. When multiple nested programs contribute to the same totals (records processed, errors found, grand total amounts), GLOBAL avoids passing these counters through every CALL USING clause.

Situation 3: Configuration data. When all nested programs need access to the same configuration settings (processing date, batch ID, system flags), GLOBAL provides clean, read-only access without cluttering every CALL statement.

Situation 4: The error accumulation pattern. When nested programs accumulate errors in a shared table (as in MedClaim's validation routines), GLOBAL on the error table enables a clean BUILD-ERRORS utility that all validators can call.

When GLOBAL Is the Wrong Choice

Wrong for business data. Passing customer records, account balances, and transaction details via GLOBAL makes data flow invisible. When debugging, you cannot trace where a value came from by reading the CALL statement — you have to understand the entire nesting structure.

Wrong for large, frequently modified structures. If multiple nested programs modify the same GLOBAL structure, the order of calls determines the final state. This creates fragile code where reordering CALL statements changes behavior.

Wrong when encapsulation matters. If a nested program should not have access to certain data, GLOBAL defeats the purpose. There is no way to make a GLOBAL item visible to some children but not others.

The GLOBAL Design Rule

Maria Chen's rule at GlobalBank: "If I would print it on the office whiteboard for everyone to see, it can be GLOBAL. Processing date? GLOBAL. Audit log? GLOBAL. A customer's Social Security number? Parameter. An account balance being modified? Parameter."

💡 Practitioner's Insight: "I use GLOBAL sparingly — mainly for files and file status fields. For data that needs to flow between programs, I prefer explicit parameter passing. It's more verbose, but six months later, when someone is debugging a problem, they can trace the data flow by reading the CALL statements. With GLOBAL, the data just magically appears in nested programs, and tracing its origin requires understanding the entire nesting structure." — Maria Chen

24.6 Scope Rules Summary

Here is a comprehensive summary of what is visible where in a nested program structure:

Data Visibility

Item Defined In Visible To
Non-GLOBAL data Program X Program X only
GLOBAL data Program X Program X + all programs directly or indirectly contained in X
LINKAGE SECTION data Program X Program X only (after receiving via CALL USING)
LOCAL-STORAGE data Program X Program X only (reinitialized on each invocation)

Program Visibility (Call Scope)

Caller Can Call
Parent Its directly contained programs
Sibling Only COMMON programs at the same level
Child Its own directly contained programs + COMMON siblings
Any program External programs (outside the nesting structure)

Name Resolution

When a program references a data item name:

  1. First, the compiler looks in the program's own DATA DIVISION.
  2. If not found, it looks for a GLOBAL item with that name in the directly containing program.
  3. If still not found, it looks in the next outer containing program, and so on, up to the outermost program.
  4. If a local name matches a GLOBAL name, the local definition wins (shadowing).
       IDENTIFICATION DIVISION.
       PROGRAM-ID. OUTER.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-VALUE    PIC X(10) VALUE 'OUTER' IS GLOBAL.

       PROCEDURE DIVISION.
           DISPLAY 'Outer sees: ' WS-VALUE
           CALL 'INNER'
           DISPLAY 'Outer still sees: ' WS-VALUE
           STOP RUN.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. INNER.
           DATA DIVISION.
           WORKING-STORAGE SECTION.
      *    This shadows the GLOBAL WS-VALUE
           01  WS-VALUE    PIC X(10) VALUE 'INNER'.

           PROCEDURE DIVISION.
      *    INNER sees its own WS-VALUE, not OUTER's
               DISPLAY 'Inner sees: ' WS-VALUE
               MOVE 'CHANGED' TO WS-VALUE
      *        Only INNER's local copy is changed
               GOBACK.
           END PROGRAM INNER.

       END PROGRAM OUTER.

Output:

Outer sees: OUTER
Inner sees: INNER
Outer still sees: OUTER

INNER's modification did not affect OUTER's WS-VALUE because INNER has its own local variable with the same name that shadows the GLOBAL one.

24.7 Worked Example: Scope Resolution in Action

To make the scope rules concrete, let us trace through a detailed example. Consider this nested structure:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PAYROLL.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COMPANY-NAME    PIC X(30) VALUE 'ACME CORP'
                              IS GLOBAL.
       01  WS-TAX-RATE        PIC S9(3)V9(4) COMP-3
                              VALUE 0.0765 IS GLOBAL.
       01  WS-RUN-DATE        PIC 9(8) IS GLOBAL.
       01  WS-EMPLOYEE-COUNT  PIC 9(5) VALUE 0.
       01  WS-TOTAL-PAYROLL   PIC S9(11)V99 COMP-3
                              VALUE 0 IS GLOBAL.

       PROCEDURE DIVISION.
       MAIN-LOGIC.
           MOVE FUNCTION CURRENT-DATE(1:8) TO WS-RUN-DATE
           CALL 'CALC-PAY'
           CALL 'CALC-TAX'
           CALL 'PRINT-CHECK'
           STOP RUN.

      * --- Nested programs below ---

       IDENTIFICATION DIVISION.
       PROGRAM-ID. CALC-PAY.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-HOURS        PIC 9(3)V9 VALUE 80.0.
       01  WS-RATE          PIC S9(5)V99 COMP-3 VALUE 25.00.
       PROCEDURE DIVISION.
           COMPUTE WS-TOTAL-PAYROLL = WS-HOURS * WS-RATE
      *    WS-TOTAL-PAYROLL is GLOBAL — modification visible
      *    to PAYROLL, CALC-TAX, and PRINT-CHECK
           GOBACK.
       END PROGRAM CALC-PAY.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. CALC-TAX.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TAX-AMOUNT   PIC S9(9)V99 COMP-3.
       01  WS-TAX-RATE      PIC S9(3)V9(4) COMP-3.
      *    ^^^ This LOCAL WS-TAX-RATE shadows the GLOBAL one!
       PROCEDURE DIVISION.
           MOVE 0.05 TO WS-TAX-RATE
      *    This moves 0.05 to the LOCAL WS-TAX-RATE,
      *    NOT the GLOBAL one (which remains 0.0765)
           COMPUTE WS-TAX-AMOUNT =
               WS-TOTAL-PAYROLL * WS-TAX-RATE
      *    Uses LOCAL WS-TAX-RATE (0.05), not GLOBAL (0.0765)
      *    WS-TOTAL-PAYROLL is GLOBAL — read access works
           GOBACK.
       END PROGRAM CALC-TAX.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PRINT-CHECK IS COMMON.
       PROCEDURE DIVISION.
           DISPLAY 'Company: ' WS-COMPANY-NAME
      *    WS-COMPANY-NAME is GLOBAL — visible here
           DISPLAY 'Pay date: ' WS-RUN-DATE
      *    WS-RUN-DATE is GLOBAL — visible here
           DISPLAY 'Gross pay: ' WS-TOTAL-PAYROLL
      *    WS-TOTAL-PAYROLL is GLOBAL — visible here
           DISPLAY 'Tax rate: ' WS-TAX-RATE
      *    WS-TAX-RATE is GLOBAL — visible here (0.0765)
      *    Note: PRINT-CHECK sees the GLOBAL tax rate (0.0765),
      *    not CALC-TAX's local shadow (0.05)!
           GOBACK.
       END PROGRAM PRINT-CHECK.

       END PROGRAM PAYROLL.

Tracing through the scope resolution:

  1. CALC-PAY accesses WS-TOTAL-PAYROLL (GLOBAL from PAYROLL). No local definition exists, so the GLOBAL is used. The modification (COMPUTE) changes the GLOBAL value, visible to all other nested programs.

  2. CALC-TAX defines a local WS-TAX-RATE that shadows the GLOBAL one. When it moves 0.05 to WS-TAX-RATE, only the local copy changes. The GLOBAL WS-TAX-RATE remains 0.0765. However, WS-TOTAL-PAYROLL has no local shadow, so CALC-TAX reads the GLOBAL value set by CALC-PAY.

  3. PRINT-CHECK has no local definitions. All data references resolve to GLOBAL items: WS-COMPANY-NAME, WS-RUN-DATE, WS-TOTAL-PAYROLL, and WS-TAX-RATE. It sees WS-TAX-RATE as 0.0765 (the GLOBAL value), not 0.05 (CALC-TAX's local value that no longer exists after CALC-TAX returned).

This example illustrates why GLOBAL shadowing can be dangerous: CALC-TAX uses a different tax rate than PRINT-CHECK displays, because CALC-TAX has a local shadow that PRINT-CHECK does not. If a developer does not realize the shadow exists, they may assume all programs use the same rate.

⚠️ Warning: GLOBAL shadowing is one of the most subtle bugs in nested COBOL programs. The compiler does not warn you when a local definition shadows a GLOBAL. Prevention strategies include: (1) using distinctive prefixes for GLOBAL items (e.g., G-TAX-RATE), (2) avoiding GLOBAL for items that any nested program might independently define, and (3) documenting all GLOBAL items prominently in the source header.

24.8 Advantages of Nested Programs

1. Single Compilation Unit

All programs are compiled together. No separate compile-link-deploy cycle for each subprogram. This simplifies the build process significantly.

External subprograms:
  Compile OUTER.cbl → OUTER.obj  ─┐
  Compile INNER.cbl → INNER.obj  ─┼→ Link-edit → OUTER (load module)
  Compile UTIL.cbl  → UTIL.obj   ─┘

Nested programs:
  Compile OUTER.cbl → OUTER.obj → Link-edit → OUTER (load module)
  (Everything is in one source file)

2. Guaranteed Interface Consistency

Because all programs are in the same source file, changes to shared copybooks or data structures are immediately visible to all programs. There is no risk of one program being compiled with an old version of a copybook.

3. GLOBAL Data Sharing

When multiple nested programs need access to the same file or counter, GLOBAL provides direct access without parameter passing. This is especially useful for: - Shared output files (report files, audit files) - Counters and accumulators - Configuration flags

4. Encapsulation

Nested programs are invisible to the outside world. Only the outermost program can be called externally. The nested programs are implementation details — internal workers that cannot be called by programs outside the source file.

5. Namespace Isolation

Two different outer programs can each contain a nested program named UTILITY without conflict. The nested program names are scoped to their containing program.

6. Performance Benefits

Calls to nested programs resolve within the same load module — there is no runtime module search or dynamic load. The overhead is comparable to a static CALL and is consistently fast regardless of load library configuration. While the performance difference is rarely significant for individual calls, it can matter when a nested utility is called thousands of times per record in a high-volume batch job.

7. Self-Contained Documentation

All related programs are in one source file. A developer reading the file sees the complete picture — the outer program's logic, the nested programs' implementations, the GLOBAL data items, and how everything connects. There is no need to search through multiple source libraries to understand the system.

24.9 Disadvantages of Nested Programs

1. Source File Size

All programs live in one source file. For complex systems, this file can become very large:

External approach: 7 source files × ~500 lines = 3,500 lines total
Nested approach:   1 source file × 3,500 lines = one very large file

Large source files are harder to navigate, harder to search, and harder for version control systems to merge.

2. All-or-Nothing Compilation

Any change to any nested program requires recompiling the entire source file. With external subprograms, you only recompile the modified module.

3. No Independent Deployment

You cannot deploy an updated nested program without deploying the entire load module. With external subprograms, you can deploy an updated module independently.

4. GLOBAL Coupling

GLOBAL data creates implicit dependencies that are hard to trace. A change to a GLOBAL item in the outer program can affect every nested program that uses it — and it is not always obvious which programs are affected.

5. Limited Reuse

A nested program cannot be called from outside its containing program. If UTILITY is nested inside ORDER-PROC, it cannot be shared with INVOICE-PROC or SHIP-PROC. External subprograms can be shared across any number of callers.

6. Debugging Complexity

When debugging a nested program structure, the debugger shows a single load module with multiple program IDs. Call stacks and storage displays can be more complex than with separate load modules.

24.9 When to Use Nested vs. External Subprograms

Here is the decision framework:

Use NESTED PROGRAMS when:
├── The subprograms are tightly coupled to the outer program
│   and are not needed anywhere else
├── You want a single compilation/deployment unit
├── The programs share files or counters naturally (GLOBAL)
├── The total source is manageable (under ~3,000 lines)
├── You want to hide implementation details from the rest
│   of the system
└── The programs are truly internal utilities of the outer
    program

Use EXTERNAL SUBPROGRAMS when:
├── The subprogram is reusable across multiple callers
├── The subprogram changes independently of its callers
├── You need independent deployment
├── The source file would be too large with nesting
├── You need the flexibility of dynamic CALL
├── Multiple developers work on different subprograms
└── The subprogram could be replaced by a different
    implementation (e.g., rewritten in another language)

⚖️ The Readability Spectrum: "I think of nested programs as a way to organize a single program's internal complexity. If the nested programs are truly internal utilities — formatting routines, local calculations, helper functions — nesting makes the source file self-contained and clear. But if they are independent business functions that happen to be called from one place today, they should be external. Tomorrow, someone else will need to call them." — Priya Kapoor

📊 Industry Usage: In a survey of Enterprise COBOL production systems, approximately 15% of programs use nested programs. The remaining 85% use external subprograms exclusively. Nested programs are most common in report generation, data transformation, and single-purpose batch utilities.

A Decision Flowchart

When you are uncertain, follow this flowchart:

Q1: Will this subprogram be called by programs
    outside the current source file?
    YES → External subprogram
    NO  → Continue to Q2

Q2: Does this subprogram change independently of
    the outer program (different release schedule)?
    YES → External subprogram
    NO  → Continue to Q3

Q3: Will the combined source file exceed 2,000 lines?
    YES → Consider external subprogram
    NO  → Continue to Q4

Q4: Does this subprogram share files or counters
    with sibling programs?
    YES → Nested program with GLOBAL (natural fit)
    NO  → Continue to Q5

Q5: Is this a small, focused utility (under 100 lines)?
    YES → Nested program (ideal case)
    NO  → Either approach works; default to external
          for flexibility

⚖️ The Readability Spectrum: The choice between nested and external subprograms is not just technical — it is about how you want the system to be understood. Nested programs say: "This is an implementation detail of the outer program." External subprograms say: "This is an independent service that happens to be called from here." Both messages are valid; choose the one that truthfully represents the relationship.

24.10 GlobalBank Case Study: Nested Utilities within ACCT-MAINT

After modularizing ACCT-MAINT into external subprograms (Chapter 22), Maria Chen realized that several small helper routines were only used within the ACCT-MAINT driver program. They were too small and too specific to justify separate compilation and deployment. She converted them to nested programs:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. ACCT-MAINT.
      *================================================================
      * GlobalBank Account Maintenance — Driver Program
      * External calls: ACCTREAD, ACCTVAL, ACCTCALC, ACCTUPD
      * Nested programs: FORMAT-ACCT, LOG-STEP, PRINT-TOTALS
      *================================================================

       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT AUDIT-LOG ASSIGN TO AUDITLOG
               FILE STATUS IS WS-AUDIT-STATUS.

       DATA DIVISION.
       FILE SECTION.
       FD  AUDIT-LOG IS GLOBAL.
       01  AUDIT-RECORD        PIC X(200).

       WORKING-STORAGE SECTION.
       01  WS-AUDIT-STATUS     PIC X(2) IS GLOBAL.
       01  WS-STEP-COUNT       PIC 9(4) IS GLOBAL VALUE 0.
       01  WS-ACCT-INTERFACE.
           05  WS-ACCT-ID      PIC X(10).
           05  WS-ACCT-DATA    PIC X(200).
       01  WS-RETURN-CODE      PIC S9(4) COMP.
       01  WS-FORMATTED-ACCT   PIC X(15).
       01  WS-RECORDS-READ     PIC 9(7) VALUE 0.
       01  WS-RECORDS-OK       PIC 9(7) VALUE 0.
       01  WS-RECORDS-ERR      PIC 9(7) VALUE 0.
       01  WS-EOF-FLAG         PIC 9 VALUE 0.
           88  END-OF-INPUT                VALUE 1.

       PROCEDURE DIVISION.
       MAIN-LOGIC.
           OPEN EXTEND AUDIT-LOG

      *    LOG-STEP is a nested COMMON utility
           CALL 'LOG-STEP' USING
               BY CONTENT 'Job started'
           END-CALL

           PERFORM PROCESS-ACCOUNTS
               UNTIL END-OF-INPUT

      *    PRINT-TOTALS is a nested program
           CALL 'PRINT-TOTALS' USING WS-RECORDS-READ
                                     WS-RECORDS-OK
                                     WS-RECORDS-ERR

           CALL 'LOG-STEP' USING
               BY CONTENT 'Job completed'
           END-CALL

           CLOSE AUDIT-LOG
           STOP RUN.

       PROCESS-ACCOUNTS.
      *    Call external subprogram for the heavy lifting
           CALL 'ACCTREAD' USING WS-ACCT-INTERFACE
                                 WS-RETURN-CODE
               ON EXCEPTION
                   SET END-OF-INPUT TO TRUE
                   EXIT PARAGRAPH
           END-CALL

           IF WS-RETURN-CODE = 4
               SET END-OF-INPUT TO TRUE
               EXIT PARAGRAPH
           END-IF

           ADD 1 TO WS-RECORDS-READ

      *    Use nested program for formatting
           CALL 'FORMAT-ACCT' USING WS-ACCT-ID
                                    WS-FORMATTED-ACCT

      *    Call external subprograms for business logic
           CALL 'ACCTVAL' USING WS-ACCT-INTERFACE
                                WS-RETURN-CODE

           IF WS-RETURN-CODE = 0
               CALL 'ACCTCALC' USING WS-ACCT-INTERFACE
                                     WS-RETURN-CODE
           END-IF

           IF WS-RETURN-CODE = 0
               CALL 'ACCTUPD' USING WS-ACCT-INTERFACE
                                    WS-RETURN-CODE
               IF WS-RETURN-CODE = 0
                   ADD 1 TO WS-RECORDS-OK
               ELSE
                   ADD 1 TO WS-RECORDS-ERR
               END-IF
           ELSE
               ADD 1 TO WS-RECORDS-ERR
           END-IF.

      *================================================================
      * Nested Program: FORMAT-ACCT
      * Formats an account ID with dashes: XXXX-XXXX-XX
      * Internal utility — not needed outside ACCT-MAINT.
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. FORMAT-ACCT IS COMMON.

       DATA DIVISION.
       LINKAGE SECTION.
       01  LS-RAW-ACCT        PIC X(10).
       01  LS-FORMATTED       PIC X(15).

       PROCEDURE DIVISION USING LS-RAW-ACCT
                                LS-FORMATTED.
           MOVE SPACES TO LS-FORMATTED
           STRING LS-RAW-ACCT(1:4) '-'
                  LS-RAW-ACCT(5:4) '-'
                  LS-RAW-ACCT(9:2)
               DELIMITED BY SIZE
               INTO LS-FORMATTED
           END-STRING
           GOBACK.

       END PROGRAM FORMAT-ACCT.

      *================================================================
      * Nested Program: LOG-STEP
      * Writes a timestamped entry to the GLOBAL audit log.
      * Uses GLOBAL file — no file parameter needed.
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. LOG-STEP IS COMMON.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-TIMESTAMP       PIC X(26).

       LINKAGE SECTION.
       01  LS-MESSAGE         PIC X(80).

       PROCEDURE DIVISION USING LS-MESSAGE.
           ADD 1 TO WS-STEP-COUNT
           MOVE FUNCTION CURRENT-DATE TO WS-TIMESTAMP

           STRING WS-TIMESTAMP(1:4) '-'
                  WS-TIMESTAMP(5:2) '-'
                  WS-TIMESTAMP(7:2) ' '
                  WS-TIMESTAMP(9:2) ':'
                  WS-TIMESTAMP(11:2) ':'
                  WS-TIMESTAMP(13:2) ' '
                  'Step ' WS-STEP-COUNT ' '
                  LS-MESSAGE
               DELIMITED BY SIZE
               INTO AUDIT-RECORD
           END-STRING

           WRITE AUDIT-RECORD
           GOBACK.

       END PROGRAM LOG-STEP.

      *================================================================
      * Nested Program: PRINT-TOTALS
      * Displays processing summary to SYSOUT.
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. PRINT-TOTALS.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-DISPLAY-NUM     PIC Z(6)9.

       LINKAGE SECTION.
       01  LS-READ            PIC 9(7).
       01  LS-OK              PIC 9(7).
       01  LS-ERR             PIC 9(7).

       PROCEDURE DIVISION USING LS-READ LS-OK LS-ERR.
           DISPLAY '--- Processing Summary ---'
           MOVE LS-READ TO WS-DISPLAY-NUM
           DISPLAY 'Records read:    ' WS-DISPLAY-NUM
           MOVE LS-OK TO WS-DISPLAY-NUM
           DISPLAY 'Records updated: ' WS-DISPLAY-NUM
           MOVE LS-ERR TO WS-DISPLAY-NUM
           DISPLAY 'Records in error:' WS-DISPLAY-NUM
           GOBACK.

       END PROGRAM PRINT-TOTALS.

       END PROGRAM ACCT-MAINT.

Notice the hybrid approach: - External subprograms (ACCTREAD, ACCTVAL, ACCTCALC, ACCTUPD) handle the reusable business logic. - Nested programs (FORMAT-ACCT, LOG-STEP, PRINT-TOTALS) handle internal utilities specific to ACCT-MAINT. - GLOBAL is used for the audit log file and step counter — items that multiple nested programs need. - COMMON is used for FORMAT-ACCT and LOG-STEP so they could be called from other nested programs if needed.

💡 Practitioner's Insight: "The hybrid approach gives us the best of both worlds. The reusable business logic is external and shared across many programs. The internal plumbing — formatting, logging, summary printing — is nested and self-contained. When we ship a new version of ACCT-MAINT, everything it needs internally travels in one load module." — Maria Chen

24.11 MedClaim Case Study: Nested Validation Routines within CLM-INTAKE

MedClaim's claim intake program (CLM-INTAKE) performs a series of validation checks on incoming claims. James Okafor organized the validation routines as nested programs because they contain business rules specific to intake processing:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. CLM-INTAKE.
      *================================================================
      * MedClaim Claims Intake — accepts and validates new claims
      * External calls: DTEVALID, DIAGLKUP, PROCLKUP
      * Nested programs: VAL-MEMBER, VAL-PROVIDER,
      *                  VAL-AMOUNTS, VAL-CODES, BUILD-ERRORS
      *================================================================

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-CLAIM-RECORD.
           05  WS-CLAIM-ID       PIC X(15).
           05  WS-MEMBER-ID      PIC X(12).
           05  WS-PROVIDER-ID    PIC X(10).
           05  WS-SERVICE-DATE   PIC 9(8).
           05  WS-DIAG-CODE      PIC X(7) OCCURS 5.
           05  WS-PROC-CODE      PIC X(5) OCCURS 10.
           05  WS-CHARGE-AMT     PIC S9(7)V99 COMP-3
                                 OCCURS 10.

       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-VALIDATION-RC      PIC S9(4) COMP.
       01  WS-OVERALL-RC         PIC S9(4) COMP VALUE 0.
       01  WS-DATE-RC            PIC 9(2).
       01  WS-DATE-MSG           PIC X(50).

       PROCEDURE DIVISION.
       INTAKE-MAIN.
      *    Perform all validations
           MOVE 0 TO WS-ERROR-COUNT

      *    Nested program validations (claim-specific)
           CALL 'VAL-MEMBER' USING WS-MEMBER-ID
                                   WS-VALIDATION-RC
           PERFORM UPDATE-OVERALL-RC

           CALL 'VAL-PROVIDER' USING WS-PROVIDER-ID
                                     WS-VALIDATION-RC
           PERFORM UPDATE-OVERALL-RC

      *    External subprogram (shared utility)
           CALL 'DTEVALID' USING WS-SERVICE-DATE
                                 WS-DATE-RC
                                 WS-DATE-MSG
           IF WS-DATE-RC > 0
               CALL 'BUILD-ERRORS' USING
                   BY CONTENT 'SERVICE-DATE'
                   BY CONTENT WS-DATE-MSG
                   BY CONTENT WS-DATE-RC
           END-IF

      *    More nested validations
           CALL 'VAL-AMOUNTS' USING WS-CHARGE-AMT(1)
                                    WS-VALIDATION-RC
           PERFORM UPDATE-OVERALL-RC

           CALL 'VAL-CODES' USING WS-DIAG-CODE(1)
                                  WS-PROC-CODE(1)
                                  WS-VALIDATION-RC
           PERFORM UPDATE-OVERALL-RC

      *    Report results
           IF WS-ERROR-COUNT > 0
               DISPLAY 'Claim ' WS-CLAIM-ID
                       ' has ' WS-ERROR-COUNT ' errors'
           ELSE
               DISPLAY 'Claim ' WS-CLAIM-ID ' accepted'
           END-IF

           STOP RUN.

       UPDATE-OVERALL-RC.
           IF WS-VALIDATION-RC > WS-OVERALL-RC
               MOVE WS-VALIDATION-RC TO WS-OVERALL-RC
           END-IF.

      *================================================================
      * Nested: VAL-MEMBER — Validate member ID format and status
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. VAL-MEMBER IS COMMON.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-NUM-CHECK        PIC 9(12).
       LINKAGE SECTION.
       01  LS-MEMBER-ID        PIC X(12).
       01  LS-RC               PIC S9(4) COMP.
       PROCEDURE DIVISION USING LS-MEMBER-ID LS-RC.
           MOVE 0 TO LS-RC

           IF LS-MEMBER-ID = SPACES
               CALL 'BUILD-ERRORS' USING
                   BY CONTENT 'MEMBER-ID'
                   BY CONTENT 'Member ID is required'
                   BY CONTENT 8
               MOVE 8 TO LS-RC
               GOBACK
           END-IF

      *    Check format: must be all numeric
           MOVE LS-MEMBER-ID TO WS-NUM-CHECK
           IF WS-NUM-CHECK IS NOT NUMERIC
               CALL 'BUILD-ERRORS' USING
                   BY CONTENT 'MEMBER-ID'
                   BY CONTENT 'Member ID must be numeric'
                   BY CONTENT 8
               MOVE 8 TO LS-RC
           END-IF

           GOBACK.
       END PROGRAM VAL-MEMBER.

      *================================================================
      * Nested: VAL-PROVIDER — Validate provider ID
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. VAL-PROVIDER IS COMMON.
       DATA DIVISION.
       LINKAGE SECTION.
       01  LS-PROVIDER-ID      PIC X(10).
       01  LS-RC               PIC S9(4) COMP.
       PROCEDURE DIVISION USING LS-PROVIDER-ID LS-RC.
           MOVE 0 TO LS-RC

           IF LS-PROVIDER-ID = SPACES
               CALL 'BUILD-ERRORS' USING
                   BY CONTENT 'PROVIDER-ID'
                   BY CONTENT 'Provider ID is required'
                   BY CONTENT 8
               MOVE 8 TO LS-RC
           END-IF

           GOBACK.
       END PROGRAM VAL-PROVIDER.

      *================================================================
      * Nested: VAL-AMOUNTS — Validate charge amounts
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. VAL-AMOUNTS IS COMMON.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-IDX              PIC 9(2).
       LINKAGE SECTION.
       01  LS-CHARGES.
           05  LS-CHARGE-AMT   PIC S9(7)V99 COMP-3
                                OCCURS 10.
       01  LS-RC               PIC S9(4) COMP.
       PROCEDURE DIVISION USING LS-CHARGES LS-RC.
           MOVE 0 TO LS-RC

           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > 10
               IF LS-CHARGE-AMT(WS-IDX) < 0
                   CALL 'BUILD-ERRORS' USING
                       BY CONTENT 'CHARGE-AMT'
                       BY CONTENT
                           'Negative charge amount'
                       BY CONTENT 8
                   MOVE 8 TO LS-RC
               END-IF
               IF LS-CHARGE-AMT(WS-IDX) > 999999.99
                   CALL 'BUILD-ERRORS' USING
                       BY CONTENT 'CHARGE-AMT'
                       BY CONTENT
                           'Charge exceeds maximum'
                       BY CONTENT 4
                   IF LS-RC < 4
                       MOVE 4 TO LS-RC
                   END-IF
               END-IF
           END-PERFORM

           GOBACK.
       END PROGRAM VAL-AMOUNTS.

      *================================================================
      * Nested: VAL-CODES — Validate diagnosis and procedure codes
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. VAL-CODES IS COMMON.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-IDX              PIC 9(2).
       01  WS-HAS-DIAG         PIC 9 VALUE 0.
       01  WS-HAS-PROC         PIC 9 VALUE 0.
       LINKAGE SECTION.
       01  LS-DIAG-CODES.
           05  LS-DIAG-CODE    PIC X(7) OCCURS 5.
       01  LS-PROC-CODES.
           05  LS-PROC-CODE    PIC X(5) OCCURS 10.
       01  LS-RC               PIC S9(4) COMP.
       PROCEDURE DIVISION USING LS-DIAG-CODES
                                LS-PROC-CODES
                                LS-RC.
           MOVE 0 TO LS-RC
           MOVE 0 TO WS-HAS-DIAG
           MOVE 0 TO WS-HAS-PROC

      *    Must have at least one diagnosis code
           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > 5
               IF LS-DIAG-CODE(WS-IDX) NOT = SPACES
                   MOVE 1 TO WS-HAS-DIAG
               END-IF
           END-PERFORM

           IF WS-HAS-DIAG = 0
               CALL 'BUILD-ERRORS' USING
                   BY CONTENT 'DIAG-CODE'
                   BY CONTENT
                       'At least one diagnosis required'
                   BY CONTENT 8
               MOVE 8 TO LS-RC
           END-IF

      *    Must have at least one procedure code
           PERFORM VARYING WS-IDX FROM 1 BY 1
               UNTIL WS-IDX > 10
               IF LS-PROC-CODE(WS-IDX) NOT = SPACES
                   MOVE 1 TO WS-HAS-PROC
               END-IF
           END-PERFORM

           IF WS-HAS-PROC = 0
               CALL 'BUILD-ERRORS' USING
                   BY CONTENT 'PROC-CODE'
                   BY CONTENT
                       'At least one procedure required'
                   BY CONTENT 8
               MOVE 8 TO LS-RC
           END-IF

           GOBACK.
       END PROGRAM VAL-CODES.

      *================================================================
      * Nested: BUILD-ERRORS — Accumulates errors in GLOBAL table
      * Uses GLOBAL WS-ERROR-TABLE from outer program.
      *================================================================
       IDENTIFICATION DIVISION.
       PROGRAM-ID. BUILD-ERRORS IS COMMON.
       DATA DIVISION.
       LINKAGE SECTION.
       01  LS-FIELD            PIC X(20).
       01  LS-MSG              PIC X(50).
       01  LS-SEV              PIC 9(2).
       PROCEDURE DIVISION USING LS-FIELD LS-MSG LS-SEV.
      *    WS-ERROR-TABLE is GLOBAL — accessible here
           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)
           END-IF
           GOBACK.
       END PROGRAM BUILD-ERRORS.

       END PROGRAM CLM-INTAKE.

Key design patterns in this example:

  1. GLOBAL error table: The WS-ERROR-TABLE is GLOBAL so all validation routines can accumulate errors through BUILD-ERRORS without passing the table as a parameter to every routine.
  2. COMMON validators: All validation routines are COMMON so they can call BUILD-ERRORS (a sibling).
  3. Hybrid calling: External utilities (DTEVALID) are called for shared functionality. Nested programs handle intake-specific validation.
  4. Error accumulation: Unlike the external call pattern where each module returns one error, the nested structure accumulates all errors in a shared table.

24.13 Comparing the Two Approaches Side by Side

To make the decision between nested and external subprograms concrete, here is the same system implemented both ways.

Scenario: A Small Invoice Processing System

The system reads invoices, validates them, calculates totals, and generates a summary report.

External Subprogram Approach:

Source files (5):
  INVCPROC.cbl   — Driver program (250 lines)
  INVCVAL.cbl    — Invoice validation (150 lines)
  INVCCALC.cbl   — Total calculation (100 lines)
  INVCRPT.cbl    — Summary report (200 lines)
  INVCIFCP.cpy   — Interface copybook (30 lines)

Compilation: 4 separate compiles
Link-edit:   4 separate modules (or 1 if statically linked)
Deployment:  4 separate deployable units
Total lines: 730 across 5 files

Nested Program Approach:

Source files (1):
  INVCPROC.cbl   — Everything in one file (700 lines)
    ├── INVCVAL   (nested, COMMON)
    ├── INVCCALC  (nested, COMMON)
    └── INVCRPT   (nested)

Compilation: 1 compile
Link-edit:   1 module
Deployment:  1 deployable unit
Total lines: 700 in 1 file (slightly less due to
             eliminated copybook overhead)

Decision factors for this scenario:

Factor External Nested Winner
File count 5 files 1 file Nested (simpler)
Independent deployment Yes No External
Concurrent development 4 developers 1 developer External
Reusability Any program can call Only INVCPROC External
GLOBAL file sharing N/A (pass parameters) Yes (simpler) Nested
Build complexity 4 compiles + link 1 compile Nested
Source navigation Multiple files One file Depends on preference

For this small system where reusability is not needed and only one or two developers work on it, nested programs are the better choice. The single-file simplicity outweighs the deployment flexibility that external subprograms offer.

If INVCVAL or INVCCALC were needed by other systems, external subprograms would be the right choice because nested programs cannot be called from outside their containing program.

24.14 Try It Yourself: Building a Nested Report Generator

🧪 Try It Yourself: Create a nested program structure for a simple report generator.

Requirements:

  1. Outer program (RPTGEN): Opens the report file, reads input data, calls nested programs, closes the file.

  2. Nested programs: - PRINT-HDR (COMMON): Prints report header with date - PRINT-DTL (COMMON): Prints a detail line - PRINT-FTR: Prints report footer with totals - FORMAT-AMT (COMMON): Formats a numeric amount for display

  3. Use GLOBAL for: - The report file (FD) - Line counter - Page counter - Lines-per-page constant (55)

  4. Logic: - Print header on first page - Print detail lines (use hardcoded test data for 10 items) - When line counter exceeds lines-per-page, print footer, advance page, print header - At end, print final footer with grand totals

This exercise practices nested program structure, COMMON attribute, GLOBAL data sharing, and the coordination between multiple nested programs.

Hints for the Try It Yourself

  • The outer program should define the report file as FD ... IS GLOBAL and open/close it.
  • Line counter, page counter, and lines-per-page should all be GLOBAL.
  • FORMAT-AMT is COMMON because both PRINT-DTL and PRINT-FTR need to call it.
  • PRINT-HDR and PRINT-DTL are COMMON because the outer program needs to call them, and they are siblings.
  • PRINT-FTR can be non-COMMON if only the outer program calls it, or COMMON if PRINT-DTL should trigger a footer when a page fills up.

Here is a skeleton to get you started:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. RPTGEN.

       ENVIRONMENT DIVISION.
       INPUT-OUTPUT SECTION.
       FILE-CONTROL.
           SELECT REPORT-FILE ASSIGN TO 'REPORT.OUT'
               FILE STATUS IS WS-FILE-STATUS.

       DATA DIVISION.
       FILE SECTION.
       FD  REPORT-FILE IS GLOBAL.
       01  REPORT-RECORD      PIC X(80).

       WORKING-STORAGE SECTION.
       01  WS-FILE-STATUS  PIC X(2) IS GLOBAL.
       01  WS-LINE-COUNT   PIC 9(3) IS GLOBAL VALUE 0.
       01  WS-PAGE-COUNT   PIC 9(3) IS GLOBAL VALUE 0.
       01  WS-LINES-PP     PIC 9(3) IS GLOBAL VALUE 20.
       01  WS-GRAND-TOTAL  PIC S9(9)V99 COMP-3
                           IS GLOBAL VALUE 0.
      * Add test data items here...

       PROCEDURE DIVISION.
       MAIN-LOGIC.
           OPEN OUTPUT REPORT-FILE
           CALL 'PRINT-HDR'
      *    Loop through test data, calling PRINT-DTL
      *    Check WS-LINE-COUNT >= WS-LINES-PP for page breaks
           CALL 'PRINT-FTR'
           CLOSE REPORT-FILE
           STOP RUN.

      * Define nested programs PRINT-HDR, PRINT-DTL,
      * PRINT-FTR, FORMAT-AMT here...

       END PROGRAM RPTGEN.

The key learning objective is understanding how GLOBAL allows multiple nested programs to coordinate on a shared resource (the report file and its associated counters) without parameter passing.

24.13 Nested Programs and the Build Process

Understanding how nested programs interact with the compile-link-deploy pipeline helps you make informed architectural decisions.

Compilation

The entire source file — outer program plus all nested programs — is compiled in a single compiler invocation:

cobol-compiler PAYROLL.cbl → PAYROLL.obj (one object module)

All nested programs are compiled together. If any nested program has a syntax error, the entire compilation fails. This is both an advantage (errors caught early) and a disadvantage (a typo in one nested program blocks compilation of all programs in the file).

The object module containing the outer program and all nested programs is link-edited into a single load module:

linker PAYROLL.obj → PAYROLL (one load module)

For statically called external subprograms, the linker also includes those object modules. But the nested programs are already in the PAYROLL object module — they do not need separate inclusion.

Deployment

Deploying a change to any nested program requires deploying the entire load module. This means:

Scenario: You change PRINT-CHECK (a nested program)
  1. Recompile PAYROLL.cbl (compiles everything)
  2. Relink PAYROLL (creates new load module)
  3. Deploy PAYROLL (replaces entire module)
  Result: All nested programs get redeployed

Compare with external subprograms:

Scenario: You change PRINTCHK (an external subprogram)
  1. Recompile PRINTCHK.cbl (compiles only PRINTCHK)
  2. Relink PRINTCHK (creates new PRINTCHK module)
  3. Deploy PRINTCHK (replaces only PRINTCHK)
  Result: Only PRINTCHK is redeployed

Impact on Source Management

With nested programs, the entire file is a single source management entity. In Endevor, SCLM, or Git:

  • Only one developer can have the file checked out at a time (in lock-based systems)
  • Merge conflicts are more likely when multiple developers change different nested programs (in merge-based systems like Git)
  • Version history shows all changes to all nested programs together

With external subprograms, each file is independent:

  • Multiple developers can work on different subprograms simultaneously
  • Version history is specific to each subprogram
  • Promotion and migration can be per-subprogram

📊 Practical Impact: At MedClaim, James Okafor limits nested program source files to under 1,500 lines specifically because of source management friction. "Beyond 1,500 lines, the probability that two developers need to change the same file on the same day exceeds 10% per sprint. That's too much conflict for the convenience of nesting."

Nested Programs in a CI/CD Pipeline

For shops adopting DevOps practices on the mainframe, nested programs interact with CI/CD pipelines as follows:

  1. Unit testing: Each nested program should be testable. However, because nested programs cannot be called from outside their containing program, you need test drivers embedded in the outer program or special test configurations.

  2. Code coverage: Coverage tools (like IBM Debug Tool or IBM Developer for z/OS) can measure coverage at the nested program level, showing which nested programs were exercised during testing.

  3. Static analysis: Tools like SonarQube with COBOL plugins analyze nested programs individually, reporting metrics (complexity, duplication) per nested program.

24.14 Advanced Topic: Nested Programs and Recursion

COBOL 2002 and later standards support recursion for programs marked with the RECURSIVE attribute. When combined with nesting, this allows powerful patterns:

       IDENTIFICATION DIVISION.
       PROGRAM-ID. FACTORIAL-DEMO.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-INPUT         PIC 9(2) VALUE 10.
       01  WS-RESULT        PIC 9(18).

       PROCEDURE DIVISION.
           CALL 'FACTORIAL' USING WS-INPUT WS-RESULT
           DISPLAY WS-INPUT '! = ' WS-RESULT
           STOP RUN.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. FACTORIAL IS RECURSIVE.

           DATA DIVISION.
           LOCAL-STORAGE SECTION.
           01  LS-LOCAL-N       PIC 9(2).
           01  LS-SUB-RESULT    PIC 9(18).

           LINKAGE SECTION.
           01  LS-N             PIC 9(2).
           01  LS-RESULT        PIC 9(18).

           PROCEDURE DIVISION USING LS-N LS-RESULT.
               IF LS-N <= 1
                   MOVE 1 TO LS-RESULT
               ELSE
                   MOVE LS-N TO LS-LOCAL-N
                   SUBTRACT 1 FROM LS-LOCAL-N
                   CALL 'FACTORIAL' USING LS-LOCAL-N
                                         LS-SUB-RESULT
                   MULTIPLY LS-N BY LS-SUB-RESULT
                       GIVING LS-RESULT
               END-IF
               GOBACK.
           END PROGRAM FACTORIAL.

       END PROGRAM FACTORIAL-DEMO.

⚠️ Important: Recursive programs must use LOCAL-STORAGE (not WORKING-STORAGE) for local variables. LOCAL-STORAGE is allocated fresh on each invocation, while WORKING-STORAGE is shared across all invocations of the same program. Using WORKING-STORAGE in a recursive program would cause each recursive call to overwrite the previous call's data.

📊 Compiler Support: Recursive nested programs are supported in IBM Enterprise COBOL V5+ and GnuCOBOL 3.x. Earlier compilers may not support the RECURSIVE attribute.

Practical Applications of Recursion

Beyond the textbook factorial example, recursion in COBOL is useful for processing hierarchical data structures:

  1. Bill of materials (BOM) explosion: A product contains components, which contain sub-components. Recursive CALL navigates the hierarchy, accumulating costs at each level.

  2. Organizational hierarchy: Employee reporting structures form trees. A recursive program traverses from manager to reports, accumulating headcounts or budget figures.

  3. Nested XML/JSON processing: Parsing nested data structures maps naturally to recursive calls, where each level of nesting corresponds to a recursive invocation.

However, recursion in COBOL is relatively rare in practice. Most hierarchical processing in mainframe applications uses iterative approaches with explicit stacks (tables serving as stacks). The iterative approach is often preferred because:

  • It is more familiar to COBOL developers who may not have encountered recursion before.
  • It provides explicit control over stack depth (the table's OCCURS count limits the depth).
  • It avoids the risk of stack overflow from unconstrained recursion.
  • Performance is predictable — no overhead from repeated program initialization.

If you do use recursion, always include a base case that terminates the recursion and a depth limit to prevent runaway recursion:

       PROCEDURE DIVISION USING BY VALUE LS-DEPTH
                                BY REFERENCE LS-DATA
                                BY REFERENCE LS-RC.
           IF LS-DEPTH > 50
               MOVE 12 TO LS-RC
               MOVE 'Maximum recursion depth exceeded'
                   TO LS-ERROR-MSG
               GOBACK
           END-IF
           ...

24.14 Common Pitfalls with Nested Programs

Pitfall 1: Missing END PROGRAM

      * WRONG — Missing END PROGRAM for INNER
       IDENTIFICATION DIVISION.
       PROGRAM-ID. OUTER.
       ...
           IDENTIFICATION DIVISION.
           PROGRAM-ID. INNER.
           ...
      *    No END PROGRAM INNER ← Compiler error!
       END PROGRAM OUTER.

Every PROGRAM-ID must have a matching END PROGRAM.

Pitfall 2: GLOBAL Shadowing Surprises

       PROGRAM-ID. OUTER.
       01  WS-STATUS PIC X(2) IS GLOBAL VALUE '00'.

           PROGRAM-ID. INNER-A.
          *    No local WS-STATUS — uses GLOBAL
           PROCEDURE DIVISION.
               MOVE '99' TO WS-STATUS     <- Changes GLOBAL
               GOBACK.
           END PROGRAM INNER-A.

           PROGRAM-ID. INNER-B.
           01  WS-STATUS PIC X(2) VALUE 'OK'.  <- Local shadow
           PROCEDURE DIVISION.
               DISPLAY WS-STATUS           <- Displays 'OK'
      *        Does NOT see the '99' set by INNER-A
               GOBACK.
           END PROGRAM INNER-B.

INNER-B has its own WS-STATUS that shadows the GLOBAL one. Changes made by INNER-A to the GLOBAL WS-STATUS are invisible to INNER-B. This can be extremely confusing.

Pitfall 3: Forgetting COMMON for Sibling Calls

       PROGRAM-ID. OUTER.
           PROGRAM-ID. WORKER.
           PROCEDURE DIVISION.
               CALL 'HELPER'    <- COMPILE ERROR!
      *        HELPER is a sibling but NOT COMMON
               GOBACK.
           END PROGRAM WORKER.

           PROGRAM-ID. HELPER.    <- Not COMMON
           PROCEDURE DIVISION.
               GOBACK.
           END PROGRAM HELPER.
       END PROGRAM OUTER.

Solution: Add IS COMMON to HELPER's PROGRAM-ID.

Pitfall 4: Assuming GLOBAL Means "Everywhere"

GLOBAL data flows downward in the nesting hierarchy only. A nested program's GLOBAL items are not visible to its parent or siblings:

       PROGRAM-ID. OUTER.
           PROGRAM-ID. INNER.
           01  WS-INNER-GLOBAL PIC X(10) IS GLOBAL.
      *    This GLOBAL is only visible to programs
      *    nested INSIDE INNER — which there are none of.
      *    OUTER cannot see WS-INNER-GLOBAL.
           END PROGRAM INNER.
       END PROGRAM OUTER.

24.15 GnuCOBOL Compatibility Notes

GnuCOBOL supports nested programs with these notes:

  1. Compilation: The entire nested structure is compiled as one unit: bash cobc -x -o rptgen rptgen.cbl No separate compilation or linking is needed for nested programs.

  2. COMMON: Fully supported in GnuCOBOL 3.x.

  3. GLOBAL: Supported for data items and file definitions. Some edge cases with GLOBAL files may behave slightly differently than IBM Enterprise COBOL.

  4. RECURSIVE: Supported in GnuCOBOL 3.x with the RECURSIVE attribute on PROGRAM-ID.

  5. Debugging: GnuCOBOL's debugging output shows the full program hierarchy, making it straightforward to trace nested program execution.

24.17 Best Practices Checklist for Nested Programs

Before concluding, here is a checklist of best practices that summarizes the key guidance from this chapter:

Planning:
☐ Identify which routines are internal utilities (nest them)
    vs. shared services (keep external)
☐ Estimate total source file size — keep under 1,500–3,000 lines
☐ Determine which data items need GLOBAL sharing
☐ Determine which nested programs need COMMON for sibling access

Naming:
☐ Use descriptive PROGRAM-IDs that indicate the program's role
☐ Use distinctive prefixes for GLOBAL items (e.g., G- prefix)
☐ Document the nesting structure in the source file header

GLOBAL Usage:
☐ Use GLOBAL for files and file status — avoids passing file
    handles as parameters
☐ Use GLOBAL for shared counters and accumulators
☐ Avoid GLOBAL for business data — pass explicitly via CALL USING
☐ Document every GLOBAL item in the header
☐ Watch for shadowing — avoid naming local items the same as
    GLOBAL items

COMMON Usage:
☐ Mark utility programs COMMON only if siblings need to call them
☐ Do not mark all programs COMMON by default — use it intentionally
☐ Document which programs are COMMON and why

Structure:
☐ Every PROGRAM-ID has a matching END PROGRAM
☐ END PROGRAM markers nest properly (innermost closed first)
☐ Each nested program has its own WORKING-STORAGE
☐ Each nested program uses GOBACK (not STOP RUN)
☐ Copybooks are used for shared interface definitions
☐ COMMON utility programs are placed at the end of the source file
    for readability

Testing:
☐ Test each nested program through the outer program
☐ Verify GLOBAL interactions between nested programs
☐ Test GLOBAL shadowing scenarios if any local names match
    GLOBAL names
☐ Verify COMMON calling works for all intended sibling pairs

24.18 Chapter Summary

This chapter has explored COBOL's nested program facility:

  • Nested (contained) programs are complete COBOL programs defined within another program's source code. They compile as a single unit and share a single load module.

  • END PROGRAM markers are mandatory and must be properly nested — innermost closed first.

  • Default calling scope: A program can call its directly contained programs. Siblings cannot call each other without the COMMON attribute.

  • The COMMON attribute makes a nested program callable by its siblings (other programs at the same nesting level), enabling shared utility patterns.

  • The GLOBAL clause makes data items and files visible to all programs contained within the declaring program, without parameter passing. Use sparingly — it creates implicit coupling.

  • Scope rules: Local definitions shadow GLOBAL ones. GLOBAL flows downward only (parent to children, not upward or sideways).

  • Advantages: Single compilation unit, guaranteed interface consistency, encapsulation, GLOBAL data sharing.

  • Disadvantages: Large source files, all-or-nothing compilation, no independent deployment, GLOBAL coupling, limited reusability.

  • Decision framework: Use nested programs for internal utilities tightly coupled to the outer program. Use external subprograms for reusable, independently deployable business logic.

Additional concepts covered in this chapter:

  • Nesting depth: While arbitrary depth is supported, practical systems rarely exceed two levels. Deep nesting is reserved for programs with internal utilities that have their own helpers.
  • GLOBAL design rules: Use GLOBAL for infrastructure (files, counters, configuration), not for business data. The "whiteboard test" — if you would post it publicly, it can be GLOBAL.
  • Build process impact: Nested programs compile and deploy as a single unit, simplifying the build but requiring full recompilation for any change.
  • Source management: Keep nested source files under 1,500-3,000 lines to avoid developer conflicts and navigation difficulty.
  • The comparison framework: Side-by-side analysis of the same system implemented with nested vs. external subprograms reveals the trade-offs concretely.
  • Decision flowchart: Five questions guide the nested-vs-external choice, from reusability and deployment to source file size and data sharing patterns.

The hybrid approach — external subprograms for shared business logic, nested programs for internal utilities — combines the strengths of both patterns and is the recommended practice for modern COBOL development. As the Readability is a Feature theme emphasizes, the goal is not to use one approach exclusively, but to choose the approach that most clearly communicates the relationship between programs: internal details are nested, shared services are external.


"Programs are like organizations. Some things should be public — shared services that everyone uses. Other things should be internal — the way a specific department organizes its work. Nested programs are the internal departments. External subprograms are the shared services. Getting the boundary right is the architect's art." — Priya Kapoor

In Chapter 25, we will explore copybook management and interface design — the techniques that keep modular COBOL systems maintainable as they grow to hundreds of programs and thousands of interface points.