> "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...
In This Chapter
- 24.1 What Are Nested Programs?
- 24.2 The Structure of Nested Programs
- 24.3 Nesting Depth and Practical Limits
- 24.4 Data Scope in Nested Programs
- 24.4 The COMMON Attribute
- 24.5 The GLOBAL Clause
- 24.6 Scope Rules Summary
- 24.7 Worked Example: Scope Resolution in Action
- 24.8 Advantages of Nested Programs
- 24.9 Disadvantages of Nested Programs
- 24.9 When to Use Nested vs. External Subprograms
- 24.10 GlobalBank Case Study: Nested Utilities within ACCT-MAINT
- 24.11 MedClaim Case Study: Nested Validation Routines within CLM-INTAKE
- 24.13 Comparing the Two Approaches Side by Side
- 24.14 Try It Yourself: Building a Nested Report Generator
- 24.13 Nested Programs and the Build Process
- 24.14 Advanced Topic: Nested Programs and Recursion
- 24.14 Common Pitfalls with Nested Programs
- 24.15 GnuCOBOL Compatibility Notes
- 24.17 Best Practices Checklist for Nested Programs
- 24.18 Chapter Summary
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:
- INNER-PROG is physically located inside OUTER-PROG's source code.
- Both programs have their own IDENTIFICATION, DATA, and PROCEDURE DIVISIONS.
- END PROGRAM markers are mandatory — they define the boundaries of each program.
- OUTER-PROG can CALL INNER-PROG just like it would call any subprogram.
- 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:
- A program can call its directly contained programs (its immediate children).
- A program cannot call its siblings by default.
- A program cannot call its parent.
- 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:
- Parameter passing via CALL USING (just like external subprograms)
- 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
- Only a directly contained program can be declared COMMON.
- A COMMON program is visible to its parent and all of its parent's directly contained programs (its siblings).
- A COMMON program is not visible to programs nested inside its siblings (nephews/nieces).
- 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
- A GLOBAL item is visible to the program that defines it and all programs directly or indirectly contained within it.
- A GLOBAL item is not visible to the program's parent or siblings — only downward in the nesting hierarchy.
- 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).
- GLOBAL can be applied to: - Data items in WORKING-STORAGE - File definitions (FD) - File status items
- 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:
- First, the compiler looks in the program's own DATA DIVISION.
- If not found, it looks for a GLOBAL item with that name in the directly containing program.
- If still not found, it looks in the next outer containing program, and so on, up to the outermost program.
- 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:
-
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.
-
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.
-
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:
- 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.
- COMMON validators: All validation routines are COMMON so they can call BUILD-ERRORS (a sibling).
- Hybrid calling: External utilities (DTEVALID) are called for shared functionality. Nested programs handle intake-specific validation.
- 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:
-
Outer program (RPTGEN): Opens the report file, reads input data, calls nested programs, closes the file.
-
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
-
Use GLOBAL for: - The report file (FD) - Line counter - Page counter - Lines-per-page constant (55)
-
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).
Link-Edit
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:
-
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.
-
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.
-
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:
-
Bill of materials (BOM) explosion: A product contains components, which contain sub-components. Recursive CALL navigates the hierarchy, accumulating costs at each level.
-
Organizational hierarchy: Employee reporting structures form trees. A recursive program traverses from manager to reports, accumulating headcounts or budget figures.
-
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:
-
Compilation: The entire nested structure is compiled as one unit:
bash cobc -x -o rptgen rptgen.cblNo separate compilation or linking is needed for nested programs. -
COMMON: Fully supported in GnuCOBOL 3.x.
-
GLOBAL: Supported for data items and file definitions. Some edge cases with GLOBAL files may behave slightly differently than IBM Enterprise COBOL.
-
RECURSIVE: Supported in GnuCOBOL 3.x with the RECURSIVE attribute on PROGRAM-ID.
-
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.