27 min read

> "The question is never whether to modernize. The question is how far to modernize, how fast to modernize, and what to preserve along the way." — Priya Kapoor, GlobalBank Systems Architect

Chapter 37: Migration and Modernization

"The question is never whether to modernize. The question is how far to modernize, how fast to modernize, and what to preserve along the way." — Priya Kapoor, GlobalBank Systems Architect

In the summer of 2024, GlobalBank's CIO convened a modernization task force. The mandate was clear: GLOBALBANK-CORE, the 1.2-million-line COBOL system that processed every transaction in the bank, needed a modernization strategy. The system was reliable — it hadn't had an unplanned outage in four years. It was fast — Maria Chen's performance tuning (Chapter 36) had the nightly batch completing in 75 minutes. It was correct — the testing framework (Chapter 34) and code review processes (Chapter 35) ensured quality.

But it had problems that no amount of tuning could solve. The system could not expose its services to mobile banking APIs without a custom integration layer. New developers took six months to become productive. The z/OS licensing costs were climbing 5% per year. And the COBOL developer talent pool was shrinking with every retirement.

Priya Kapoor was asked to evaluate the options. Her answer, presented to the board six weeks later, was not "rewrite everything in Java." Nor was it "leave everything as is." It was a nuanced strategy that placed different components at different points on what she called "the modernization spectrum."

This chapter explores that spectrum — from simple maintenance to full replacement — and gives you the framework to make informed modernization decisions for COBOL systems.

37.1 The Modernization Spectrum

Modernization is not a binary choice between "keep COBOL" and "rewrite in Java." It's a continuum of strategies, each with different costs, risks, and benefits:

┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ MAINTAIN │   WRAP   │  EXTEND  │ RE-ARCH  │ REPLACE  │
│          │          │          │          │          │
│ Keep as  │ Add API  │ Add new  │ Redesign │ Rewrite  │
│ is with  │ layer    │ function │ system   │ from     │
│ minimal  │ around   │ in modern│ arch but │ scratch  │
│ changes  │ existing │ language │ keep     │ in new   │
│          │ code     │          │ logic    │ language │
│          │          │          │          │          │
│ Risk: ★  │ Risk: ★★ │ Risk:★★★ │ Risk:★★★★│ Risk:★★★★★│
│ Cost: $  │ Cost: MATH0$│ Cost:$$$$│ Cost:$$$$$│
│ Time: ◯  │ Time: ◯◯ │ Time:◯◯◯ │ Time:◯◯◯◯│ Time:◯◯◯◯◯│
└──────────┴──────────┴──────────┴──────────┴──────────┘

← Lower risk, lower reward           Higher risk, higher reward →

Strategy 1: Maintain

Continue running the COBOL system on the mainframe with incremental improvements. Apply the practices from Chapters 34-36: add tests, improve code quality, tune performance.

When it works: The system meets current business needs, costs are acceptable, and skilled developers are available.

When it doesn't: Business needs require capabilities the system cannot provide (real-time APIs, mobile integration, cloud scalability).

Strategy 2: Wrap

Keep the COBOL code unchanged but expose its functionality through modern interfaces — REST APIs, message queues, or web services. The COBOL program runs on the mainframe; a thin integration layer translates between modern protocols and COBOL's native interfaces (CICS, MQ, batch files).

When it works: The core logic is sound but needs to be accessible from modern systems. Time pressure is high. Risk tolerance is low.

When it doesn't: The underlying COBOL system has fundamental architectural problems (monolithic design, hard-coded business rules) that wrapping doesn't address.

Strategy 3: Extend

Add new functionality in a modern language (Java, Python, C#) while keeping existing COBOL code for established functions. The two worlds communicate through APIs, message queues, or shared databases.

When it works: New capabilities need modern technology (machine learning, real-time analytics, mobile-first UX), but existing COBOL logic is too costly or risky to rewrite.

When it doesn't: The boundary between old and new creates integration complexity that exceeds the cost of rewriting.

Strategy 4: Re-Architect

Redesign the system architecture (e.g., move from monolithic to microservices) while preserving the business logic. The COBOL code may be refactored, recompiled for a different platform, or translated to another language using automated tools.

When it works: The architecture is the bottleneck, not the language. The business logic is well-understood and well-tested.

When it doesn't: The business logic is poorly understood or poorly documented, making any transformation risky.

Strategy 5: Replace

Rewrite the system from scratch in a modern language on a modern platform. Start from business requirements, not from existing code.

When it works: The existing system is so outdated, so poorly structured, or so misaligned with business needs that preserving any of it would be counterproductive.

When it doesn't: Almost always riskier and more expensive than expected. The "second system effect" (Fred Brooks) applies with full force.

💡 Legacy != Obsolete: A system is not "legacy" because it's written in COBOL. A system is legacy when it no longer meets business needs, when it can't be modified safely, or when the cost of maintaining it exceeds the cost of alternatives. Many COBOL systems are none of these things — they are mature, reliable, and cost-effective. The word "modernization" should not be a euphemism for "rewrite."

37.2 API Wrapping — Exposing COBOL as Services

The most common modernization approach today is wrapping — exposing existing COBOL functionality through RESTful APIs without changing the COBOL code. This gives modern applications (mobile apps, web frontends, partner integrations) access to mainframe services.

Architecture

┌─────────────────┐     ┌─────────────────┐     ┌──────────────┐
│  Mobile App     │     │  API Gateway    │     │  z/OS        │
│  Web Frontend   │────►│  (REST/JSON)    │────►│  CICS        │
│  Partner API    │     │                 │     │  COBOL       │
│                 │◄────│  Transform:     │◄────│  Programs    │
│                 │     │  JSON↔COMMAREA  │     │              │
└─────────────────┘     └─────────────────┘     └──────────────┘

IBM z/OS Connect

IBM z/OS Connect EE is the enterprise solution for exposing COBOL/CICS programs as REST APIs. It handles JSON-to-COBOL data transformation automatically:

// REST Request (JSON)
POST /api/v1/accounts/balance
{
    "accountNumber": "1234567890",
    "requestType": "INQUIRY"
}

z/OS Connect transforms this to a CICS COMMAREA:

       01  DFHCOMMAREA.
           05 CA-ACCOUNT-NUMBER    PIC X(10).
           05 CA-REQUEST-TYPE      PIC X(10).
           05 CA-RESPONSE-CODE     PIC X(4).
           05 CA-ACCOUNT-BALANCE   PIC S9(11)V99 COMP-3.
           05 CA-ACCOUNT-STATUS    PIC X.
           05 CA-LAST-TXN-DATE     PIC X(10).

The COBOL program executes normally under CICS, and z/OS Connect transforms the COMMAREA response back to JSON:

// REST Response (JSON)
{
    "responseCode": "0000",
    "accountBalance": 15432.67,
    "accountStatus": "A",
    "lastTransactionDate": "2025-10-15"
}

Custom API Wrapper Pattern

When z/OS Connect is not available, you can build a custom wrapper using a Java/Spring Boot application that communicates with CICS through IBM's CTG (CICS Transaction Gateway):

// Java Spring Boot REST controller wrapping COBOL
@RestController
@RequestMapping("/api/v1/accounts")
public class AccountController {

    @Autowired
    private CICSGateway cicsGateway;

    @PostMapping("/balance")
    public BalanceResponse getBalance(
            @RequestBody BalanceRequest request) {

        // Build COMMAREA
        byte[] commarea = new byte[50];
        System.arraycopy(
            request.getAccountNumber().getBytes(),
            0, commarea, 0, 10);
        System.arraycopy(
            "INQUIRY   ".getBytes(),
            0, commarea, 10, 10);

        // Call CICS transaction
        byte[] response = cicsGateway.execute(
            "GBIQ",     // Transaction ID
            commarea
        );

        // Parse response
        return BalanceResponse.fromCommarea(response);
    }
}

API Design Considerations

When wrapping COBOL programs as APIs, several design challenges arise:

Data type mapping: COBOL's PIC clauses don't map neatly to JSON types. Packed decimal (COMP-3), zoned decimal, and EBCDIC character encoding all require careful transformation.

COBOL Type JSON Type Notes
PIC X(n) string EBCDIC to UTF-8 conversion
PIC 9(n) number (integer) Strip leading zeros
PIC 9(n)V9(m) number (decimal) Implied decimal becomes explicit
PIC S9(n) COMP-3 number Sign handling
PIC X (88-level) boolean or enum Map condition values

Error handling: COBOL programs signal errors through return codes and status fields, not exceptions. Your wrapper must translate these:

// Map COBOL return codes to HTTP status codes
switch (responseCode) {
    case "0000": return ResponseEntity.ok(response);      // 200
    case "0004": return ResponseEntity.notFound().build(); // 404
    case "0008": return ResponseEntity.badRequest()        // 400
                    .body(errorMessage);
    case "0012": return ResponseEntity.status(500)         // 500
                    .body("Internal processing error");
}

Statelessness: COBOL/CICS programs often maintain conversational state through pseudo-conversational transactions. REST APIs are stateless. Your wrapper must manage the mismatch — either making each API call a complete transaction, or implementing session management externally.

Try It Yourself: If you have access to GnuCOBOL, write a simple COBOL program that calculates loan payments (principal, rate, term) and returns the monthly payment and total interest. Then write a Python Flask wrapper that exposes this as a REST API. Use Python's subprocess module to call the compiled GnuCOBOL program.

37.3 Screen Scraping and Terminal Emulation

Before API wrapping became standard, many organizations connected modern systems to COBOL through "screen scraping" — automating the 3270 terminal interface. While considered a legacy integration technique, screen scraping is still used in organizations that cannot modify their CICS transactions.

How It Works

A screen scraper emulates a 3270 terminal, sending keystrokes and reading screen positions as if a human were typing at a terminal:

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Modern App  │────►│  Screen      │────►│  CICS        │
│              │     │  Scraper     │     │  3270 BMS    │
│              │◄────│  Engine      │◄────│  Screens     │
└──────────────┘     └──────────────┘     └──────────────┘
                     Emulates terminal
                     interaction

Why It's Problematic

  • Fragile: Any change to screen layout breaks the integration
  • Slow: Multiple screen interactions for a single business operation
  • Unmaintainable: Screen positions are hard-coded magic numbers
  • Insecure: Credentials may be embedded in automation scripts

Screen scraping should be considered a temporary bridge, not a long-term strategy. If you encounter screen scraping in practice, plan to replace it with API wrapping.

37.4 Database Migration

Many modernization projects involve moving data from mainframe-native formats to relational databases.

IMS to DB2

IMS (Information Management System) databases use a hierarchical data model. Migrating to DB2 requires restructuring the data into relational tables:

IMS Hierarchy:              DB2 Tables:
                            ┌─────────────────┐
CUSTOMER (root)             │ CUSTOMER         │
├── ADDRESS (child)    ──►  │ customer_id PK   │
├── ACCOUNT (child)         │ name             │
│   └── TRANSACTION         │ status           │
└── PHONE (child)           └─────────────────┘
                            ┌─────────────────┐
                            │ ADDRESS          │
                            │ address_id PK    │
                            │ customer_id FK   │
                            │ type             │
                            │ street           │
                            └─────────────────┘
                            ┌─────────────────┐
                            │ ACCOUNT          │
                            │ account_id PK    │
                            │ customer_id FK   │
                            │ type             │
                            │ balance          │
                            └─────────────────┘

Key challenges: - Many-to-many relationships: IMS doesn't handle these natively; they must be inferred - Data types: IMS uses DL/I data definitions that don't map directly to SQL types - Performance: IMS sequential access patterns may not translate well to SQL queries - Program changes: Every DL/I CALL in COBOL must be replaced with EXEC SQL

Flat Files to Database

Converting sequential and VSAM files to database tables is the most common data migration:

      * BEFORE: VSAM KSDS access
       READ ACCT-MASTER-FILE
           KEY IS WS-ACCT-KEY
           INVALID KEY
               SET ACCT-NOT-FOUND TO TRUE
           NOT INVALID KEY
               SET ACCT-FOUND TO TRUE
       END-READ

      * AFTER: DB2 access
       EXEC SQL
           SELECT ACCT_NUMBER, ACCT_TYPE, ACCT_BALANCE,
                  ACCT_STATUS, ACCT_RATE
           INTO :WS-ACCT-NUMBER, :WS-ACCT-TYPE,
                :WS-ACCT-BALANCE, :WS-ACCT-STATUS,
                :WS-ACCT-RATE
           FROM ACCOUNTS
           WHERE ACCT_NUMBER = :WS-ACCT-KEY
       END-EXEC
       EVALUATE SQLCODE
           WHEN 0
               SET ACCT-FOUND TO TRUE
           WHEN 100
               SET ACCT-NOT-FOUND TO TRUE
           WHEN OTHER
               PERFORM 9500-SQL-ERROR
       END-EVALUATE

Data Migration Utilities

//*----------------------------------------------------------
//* Extract VSAM data to sequential file for migration
//*----------------------------------------------------------
//EXTRACT  EXEC PGM=IDCAMS
//SYSPRINT DD SYSOUT=*
//INPUT    DD DSN=PROD.ACCT.MASTER,DISP=SHR
//OUTPUT   DD DSN=MIGRATE.ACCT.EXTRACT,DISP=(NEW,CATLG),
//            SPACE=(CYL,(100,20)),
//            DCB=(RECFM=FB,LRECL=500,BLKSIZE=32000)
//SYSIN    DD *
  REPRO INFILE(INPUT) OUTFILE(OUTPUT)
/*
//*
//*----------------------------------------------------------
//* Load extracted data into DB2
//*----------------------------------------------------------
//LOADDB   EXEC PGM=DSNUTILB,PARM='DB2T,LOADACCT'
//SYSREC00 DD DSN=MIGRATE.ACCT.EXTRACT,DISP=SHR
//SYSIN    DD *
  LOAD DATA INDDN(SYSREC00)
       INTO TABLE ACCOUNTS
       (ACCT_NUMBER POSITION(1) CHAR(10),
        ACCT_TYPE   POSITION(11) CHAR(3),
        ACCT_BALANCE POSITION(14) DECIMAL,
        ACCT_STATUS POSITION(24) CHAR(1),
        ACCT_RATE   POSITION(25) DECIMAL)
/*

37.5 Cloud Deployment — COBOL on AWS/Azure/GCP

Running COBOL in the cloud is increasingly viable. Several approaches exist:

Approach 1: Recompile for Linux

GnuCOBOL compiles COBOL to native C, which runs on any Linux system — including cloud VMs and containers:

# Compile COBOL to executable on Linux
cobc -x -o bal-calc BAL-CALC.cbl

# Run on any Linux system
./bal-calc

This approach works well for batch COBOL programs that don't depend on z/OS-specific features (VSAM, CICS, JES). File I/O uses standard Linux file systems.

Approach 2: Managed COBOL Runtimes

Micro Focus (now part of OpenText) provides COBOL runtimes for cloud platforms:

  • Micro Focus Visual COBOL runs on Windows and Linux, supporting both batch and online COBOL
  • Micro Focus Enterprise Server provides CICS and JCL emulation on cloud VMs
  • These preserve mainframe-style programming while running on x86 hardware

Approach 3: Mainframe as a Service

IBM and third parties offer z/OS instances in the cloud:

  • IBM Wazi as a Service: z/OS development and testing environments on IBM Cloud
  • IBM Z and Cloud Modernization Center: Tools and services for hybrid mainframe-cloud architectures

Cloud Deployment Considerations

Factor On-Premises Mainframe Cloud COBOL
Licensing MIPS-based (expensive) Subscription or usage-based
Performance Dedicated hardware, optimized Shared infrastructure, variable
Reliability 99.999% (z/OS) 99.9-99.99% (cloud SLA)
Scalability Vertical (add capacity) Horizontal (add instances)
Skills needed z/OS, JCL, mainframe ops Linux, containers, cloud ops
CICS/IMS Native support Emulation or replacement
VSAM Native support File system or database

37.6 Microservices with COBOL

The idea of COBOL microservices may seem paradoxical, but it's increasingly practical. A containerized COBOL program that performs one specific function — validate an account, calculate interest, adjudicate a claim — fits the microservice definition.

Containerized COBOL

# Dockerfile for a COBOL microservice
FROM ubuntu:22.04

# Install GnuCOBOL
RUN apt-get update && \
    apt-get install -y gnucobol && \
    rm -rf /var/lib/apt/lists/*

# Copy COBOL source and compile
COPY BAL-CALC.cbl /app/
WORKDIR /app
RUN cobc -x -o bal-calc BAL-CALC.cbl

# Copy wrapper script
COPY run-service.sh /app/
RUN chmod +x /app/run-service.sh

# Expose port for health checks
EXPOSE 8080

CMD ["/app/run-service.sh"]

The wrapper script bridges between HTTP requests and the COBOL program:

#!/bin/bash
# run-service.sh - Simple wrapper for COBOL microservice
# In production, use a proper web framework

while true; do
    # Listen for requests (simplified)
    nc -l -p 8080 | while read line; do
        # Extract parameters from request
        PRINCIPAL=$(echo "$line" | jq -r '.principal')
        RATE=$(echo "$line" | jq -r '.rate')
        DAYS=$(echo "$line" | jq -r '.days')

        # Set environment variables for COBOL program
        export CALC_PRINCIPAL=$PRINCIPAL
        export CALC_RATE=$RATE
        export CALC_DAYS=$DAYS

        # Run COBOL program
        RESULT=$(./bal-calc)

        # Return response
        echo "{\"interest\": \"$RESULT\"}"
    done
done

In practice, you would use a Python/Flask or Node.js wrapper rather than shell scripting:

# Python Flask wrapper for COBOL microservice
from flask import Flask, request, jsonify
import subprocess
import os

app = Flask(__name__)

@app.route('/api/v1/interest', methods=['POST'])
def calculate_interest():
    data = request.json

    # Set environment for COBOL program
    env = os.environ.copy()
    env['CALC_PRINCIPAL'] = str(data['principal'])
    env['CALC_RATE'] = str(data['rate'])
    env['CALC_DAYS'] = str(data['days'])

    # Execute COBOL program
    result = subprocess.run(
        ['./bal-calc'],
        capture_output=True,
        text=True,
        env=env
    )

    # Parse COBOL output
    interest = float(result.stdout.strip())

    return jsonify({
        'principal': data['principal'],
        'rate': data['rate'],
        'days': data['days'],
        'interest': interest
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Kubernetes Deployment

# kubernetes deployment for COBOL microservice
apiVersion: apps/v1
kind: Deployment
metadata:
  name: interest-calculator
spec:
  replicas: 3
  selector:
    matchLabels:
      app: interest-calculator
  template:
    metadata:
      labels:
        app: interest-calculator
    spec:
      containers:
      - name: interest-calc
        image: globalbank/interest-calculator:1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: interest-calculator
spec:
  selector:
    app: interest-calculator
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP

This deploys three replicas of the COBOL interest calculator, load-balanced behind a Kubernetes service. The COBOL program itself is unchanged — only the deployment mechanism is modern.

37.7 Rewriting Strategies

Sometimes, wrapping and extending are not enough. When a system needs fundamental changes — new data models, new architectures, new capabilities — rewriting may be the right answer. But rewriting is the riskiest and most expensive modernization strategy.

When Rewriting Makes Sense

  1. The business logic has fundamentally changed — the existing code implements rules that no longer apply
  2. The architecture prevents essential capabilities — real-time processing, horizontal scaling, cloud-native deployment
  3. The codebase is unmaintainable — no tests, no documentation, no one who understands it
  4. The platform is being decommissioned — the organization is exiting mainframe entirely

When Rewriting Fails

The history of IT is littered with failed rewrite projects. Common failure modes:

The second system effect: The rewrite team tries to add new features while replicating existing functionality, leading to scope explosion.

Underestimating existing complexity: A 1-million-line COBOL system encodes decades of business rules, edge cases, and regulatory requirements. Rewriters consistently discover rules they didn't know existed.

Big bang cutover: Trying to switch from old to new in a single weekend. If anything goes wrong, there is no fallback.

Loss of institutional knowledge: The COBOL developers who understand the business rules retire or leave before the rewrite is complete.

Strangler Fig Pattern

The safest rewriting strategy is the "Strangler Fig" pattern — incrementally replacing one function at a time while keeping the old system running:

Year 1: Old system handles everything
        ┌────────────────────────────────┐
        │  COBOL: 100%                   │
        └────────────────────────────────┘

Year 2: New system handles account inquiry
        ┌──────────────────────┬─────────┐
        │  COBOL: 80%          │ Java 20%│
        └──────────────────────┴─────────┘

Year 3: New system handles inquiry + transfers
        ┌────────────────┬───────────────┐
        │  COBOL: 60%    │   Java 40%    │
        └────────────────┴───────────────┘

Year 4: New system handles most operations
        ┌────────┬───────────────────────┐
        │COBOL20%│      Java 80%         │
        └────────┴───────────────────────┘

Year 5: Complete migration (or COBOL remains for edge cases)
        ┌────────────────────────────────┐
        │         Java: 100%             │
        └────────────────────────────────┘

Each increment is small enough to be tested, validated, and rolled back if necessary. The old and new systems run in parallel, with a routing layer directing traffic.

Automated Code Translation

Several commercial tools translate COBOL to Java, C#, or other languages:

  • Micro Focus COBOL to Java: Preserves program structure, translates COBOL paragraphs to Java methods
  • TSRI (Software Mining): AI-assisted translation with business logic extraction
  • Modern Systems: Automated COBOL-to-Java/C# conversion

These tools produce functional but often unidiomatic target code. The translated Java looks like COBOL written in Java syntax — long methods, global state, imperative style. Post-translation refactoring is essential.

⚠️ Caution: Automated translation tools can convert syntax, but they cannot convert architecture. A monolithic COBOL program translated to Java is a monolithic Java program. The translation preserves all the architectural limitations of the original — it just runs on a different platform.

37.8 AI-Assisted Code Analysis

Artificial intelligence is increasingly useful for understanding legacy COBOL code — arguably its most valuable application in the modernization space.

What AI Can Do

  1. Code summarization: Generate natural-language descriptions of what a COBOL paragraph does
  2. Business rule extraction: Identify the business rules embedded in code (e.g., "claims over $50,000 require manual review")
  3. Dependency mapping: Trace data flows across programs, copybooks, and files
  4. Dead code identification: Identify unreachable code paths with greater accuracy than static analysis alone
  5. Documentation generation: Create technical documentation from undocumented code
  6. Translation assistance: Help translate COBOL to modern languages with better code quality than fully automated tools

What AI Cannot Do (Yet)

  1. Understand business context: AI can determine that a paragraph calculates a rate, but not why that rate was chosen or what regulatory requirement it satisfies
  2. Validate correctness: AI can translate code but cannot guarantee the translation preserves all edge-case behavior
  3. Replace human judgment: Modernization decisions require understanding organizational context, risk tolerance, and business strategy

Using AI Responsibly

┌─────────────────────────────────────────────────────────┐
│  AI-Assisted Modernization Workflow                     │
├─────────────────────────────────────────────────────────┤
│  1. AI analyzes COBOL code → generates documentation    │
│  2. Human reviews documentation → validates accuracy    │
│  3. AI identifies business rules → creates rule catalog │
│  4. Human validates rules → confirms with business SMEs │
│  5. AI suggests refactoring → proposes code changes     │
│  6. Human reviews changes → applies judgment on risk    │
│  7. Automated tests verify → regression suite confirms  │
└─────────────────────────────────────────────────────────┘
     AI assists.  Humans decide.  Tests verify.

🔴🔵 Debate: Rewrite or Wrap? The Eternal Question

Position A (Wrap): "Wrapping preserves proven business logic, minimizes risk, and delivers value quickly. The COBOL code works — why replace it? Wrap it in modern APIs and focus developer effort on new capabilities, not on recreating what already exists."

Position B (Rewrite): "Wrapping is a Band-Aid. It adds complexity (the wrapper layer) without addressing fundamental problems (monolithic architecture, outdated data models, dying skill pool). Every year you defer the rewrite, the cost increases. Rip off the Band-Aid."

The Pragmatic Position: The answer depends on context. For stable, well-understood business logic with clear interfaces (account validation, interest calculation, claim adjudication), wrapping is usually the right answer. For systems with fundamental architectural constraints that prevent business innovation, some degree of rewriting is necessary. The Strangler Fig pattern lets you rewrite incrementally, minimizing risk.

37.9 Risk Assessment

Every modernization strategy carries risk. The key is to identify, quantify, and mitigate risks before they become problems.

The Modernization Risk Matrix

Risk Category Maintain Wrap Extend Re-Architect Replace
Data loss Very Low Low Low Medium High
Logic errors Very Low Low Medium High Very High
Schedule overrun N/A Low Medium High Very High
Budget overrun N/A Low Medium High Very High
Business disruption Very Low Low Low Medium High
Knowledge loss Medium* Low Low Medium High
Skill dependency High* Medium Low Low Low

*Maintain has high skill dependency risk because it requires continued COBOL expertise, and medium knowledge loss risk because retiring developers take institutional knowledge with them.

Risk Mitigation Strategies

For wrapping: - Comprehensive API testing (contract tests, integration tests) - Performance testing under realistic loads - Fallback mechanism to direct mainframe access - Monitoring for data transformation errors

For rewriting: - Parallel running: old and new systems process the same data, results compared - Incremental cutover (Strangler Fig) - Business rule validation with domain experts - Extensive regression testing against production data

For all strategies: - Document all business rules before starting - Build a comprehensive test suite (Chapter 34) before making changes - Retain COBOL expertise throughout the transition - Plan for rollback at every stage

The Cost of Doing Nothing

While every modernization strategy has risks, doing nothing also has risks:

  • Talent risk: COBOL developer retirements create a growing skills gap
  • Cost risk: Mainframe licensing costs typically increase 5-8% per year
  • Capability risk: Inability to support modern business requirements (APIs, mobile, real-time)
  • Vendor risk: Reduced vendor investment in mainframe tooling and support

37.10 GlobalBank Case Study: Evaluating Modernization Options

Priya Kapoor's modernization assessment for GLOBALBANK-CORE evaluated each major subsystem independently:

The Assessment Matrix

Subsystem LOC Stability Change Freq Interfaces Recommendation
BAL-CALC 3,800 High Low Batch only Maintain + Wrap
TXN-PROC 8,200 High Medium CICS, Batch Wrap as API
ACCT-MAINT 3,650 Medium High CICS Wrap as API
RPT-DAILY 5,100 High Low Batch only Maintain
ONLINE-INQ 2,400 Low High CICS Extend (new web UI)
MOBILE-SVC 0 N/A N/A N/A New (Java/API)

The Strategy

Priya recommended a three-phase approach:

Phase 1 (6 months): Wrap TXN-PROC and ACCT-MAINT as REST APIs using z/OS Connect. This immediately enables the mobile banking project without touching COBOL code. Cost: $400K. Risk: Low.

Phase 2 (12 months): Build the new mobile banking platform (MOBILE-SVC) in Java/Spring Boot, calling the wrapped COBOL APIs for core banking functions. New capabilities (push notifications, spending analysis, card management) are built natively in Java. Cost: $1.2M. Risk: Medium.

Phase 3 (18-24 months): Evaluate whether the COBOL APIs or the new Java services should handle each function going forward. Use the Strangler Fig pattern to incrementally move functions to Java where it makes sense, keeping COBOL where it doesn't. Cost: TBD based on Phase 2 experience. Risk: Variable.

What Priya Did NOT Recommend

Priya explicitly rejected a "big bang" rewrite of GLOBALBANK-CORE:

"A rewrite of 1.2 million lines of COBOL would take 3-5 years and cost $15-25 million, based on industry benchmarks. The risk of failure is approximately 50% based on comparable projects. And during those 3-5 years, we would be maintaining two systems — the old one in production and the new one in development — doubling our operational complexity.

The wrapping approach delivers 80% of the business value (API access, mobile banking) at 10% of the cost and risk. We can always rewrite later if needed — but we probably won't need to."

37.11 MedClaim Case Study: Wrapping Claims Processing as a Microservice

MedClaim needed to give provider offices real-time access to claim status — something that previously required a phone call to the claims department. James Okafor proposed wrapping the existing CLM-STATUS inquiry program as a REST API.

The Challenge

The CLM-STATUS program was a CICS transaction that displayed claim status on a 3270 terminal. It accepted a claim number, looked up the claim in DB2, and displayed status, dates, and payment information on a BMS map.

Converting this to an API required: 1. Separating the business logic (DB2 lookup, status determination) from the presentation (BMS map) 2. Creating a new interface (COMMAREA) for the API layer 3. Building the REST wrapper

The Refactoring

James split CLM-STATUS into two programs:

      *================================================================*
      * CLM-STATUS-API: Business logic for claim status inquiry.
      * Called via COMMAREA (by API wrapper) or LINK (by CICS UI).
      * NO screen I/O in this program.
      *================================================================*
       IDENTIFICATION DIVISION.
       PROGRAM-ID. CLM-STATUS-API.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY CPY-SQLCA.
           COPY CPY-CLAIM-WS.

       LINKAGE SECTION.
       01  LS-COMMAREA.
           05 LS-REQUEST.
              10 LS-CLAIM-NUMBER   PIC X(12).
              10 LS-REQUEST-TYPE   PIC X(2).
           05 LS-RESPONSE.
              10 LS-RETURN-CODE    PIC X(4).
              10 LS-CLAIM-STATUS   PIC X(10).
              10 LS-RECEIVED-DATE  PIC X(10).
              10 LS-ADJUD-DATE     PIC X(10).
              10 LS-PAID-DATE      PIC X(10).
              10 LS-BILLED-AMOUNT  PIC S9(9)V99 COMP-3.
              10 LS-PAID-AMOUNT    PIC S9(9)V99 COMP-3.
              10 LS-REJECT-REASON  PIC X(40).

       PROCEDURE DIVISION.
       0000-MAIN.
           PERFORM 1000-LOOKUP-CLAIM
           EXEC CICS RETURN END-EXEC.

       1000-LOOKUP-CLAIM.
           EXEC SQL
               SELECT CLAIM_STATUS, RECEIVED_DATE,
                      ADJUDICATED_DATE, PAID_DATE,
                      BILLED_AMOUNT, PAID_AMOUNT,
                      REJECT_REASON
               INTO :LS-CLAIM-STATUS, :LS-RECEIVED-DATE,
                    :LS-ADJUD-DATE, :LS-PAID-DATE,
                    :LS-BILLED-AMOUNT, :LS-PAID-AMOUNT,
                    :LS-REJECT-REASON
               FROM CLAIMS
               WHERE CLAIM_NUMBER = :LS-CLAIM-NUMBER
           END-EXEC
           EVALUATE SQLCODE
               WHEN 0
                   MOVE "0000" TO LS-RETURN-CODE
               WHEN 100
                   MOVE "0004" TO LS-RETURN-CODE
                   MOVE "NOT FOUND" TO LS-CLAIM-STATUS
               WHEN OTHER
                   MOVE "0012" TO LS-RETURN-CODE
           END-EVALUATE.

The original 3270 transaction now calls CLM-STATUS-API via EXEC CICS LINK, and the API wrapper also calls it via z/OS Connect.

The API Contract

# OpenAPI specification for Claim Status API
openapi: 3.0.0
info:
  title: MedClaim Claim Status API
  version: 1.0.0
paths:
  /api/v1/claims/{claimNumber}/status:
    get:
      summary: Get claim status
      parameters:
        - name: claimNumber
          in: path
          required: true
          schema:
            type: string
            maxLength: 12
      responses:
        '200':
          description: Claim found
          content:
            application/json:
              schema:
                type: object
                properties:
                  claimNumber:
                    type: string
                  status:
                    type: string
                    enum: [RECEIVED, IN_REVIEW, ADJUDICATED,
                           PAID, REJECTED, PENDING]
                  receivedDate:
                    type: string
                    format: date
                  billedAmount:
                    type: number
                  paidAmount:
                    type: number
                  rejectReason:
                    type: string
        '404':
          description: Claim not found
        '500':
          description: Internal error

Results

The API went live in 8 weeks — from kickoff to production. Provider offices could now check claim status through a web portal instead of calling the claims department. Call volume to the claims department dropped by 35% in the first month.

"The key insight," James said, "was that we didn't need to rewrite anything. The business logic — the DB2 query, the status determination — was already correct. We just needed to separate it from the screen and put a modern interface on it."

37.12 The Strangler Fig Pattern in Detail

The Strangler Fig pattern — named after the tropical fig that gradually envelops and replaces its host tree — is the gold standard for incremental system replacement. Let us trace through a detailed implementation, using GlobalBank's TXN-PROC (transaction processing) as the example.

The Routing Layer

The heart of the Strangler Fig pattern is a routing layer that decides whether each request goes to the old system or the new system. Initially, all traffic goes to the old system. As new implementations are validated, the router shifts traffic incrementally.

┌─────────────┐     ┌──────────────────┐     ┌─────────────┐
│  Incoming   │     │  ROUTING LAYER   │     │   OLD       │
│  Request    │────►│                  │────►│   COBOL     │
│             │     │  if route_to_new │     │   TXN-PROC  │
│             │     │    → Java svc    │     └─────────────┘
│             │     │  else            │
│             │     │    → COBOL pgm   │     ┌─────────────┐
│             │     │                  │────►│   NEW       │
│             │     │  Feature flags   │     │   JAVA      │
│             │     │  control routing │     │   SERVICE   │
│             │     └──────────────────┘     └─────────────┘

Implementation Phases

Phase 0: Instrument the old system. Before replacing anything, add logging to understand exactly which transactions flow through which code paths:

      * Add instrumentation to TXN-PROC
       2000-PROCESS-TRANSACTION.
           PERFORM 2010-LOG-TXN-TYPE
           EVALUATE TXN-TYPE
               WHEN "DEP"
                   PERFORM 3000-PROCESS-DEPOSIT
               WHEN "WTH"
                   PERFORM 4000-PROCESS-WITHDRAWAL
               WHEN "XFR"
                   PERFORM 5000-PROCESS-TRANSFER
               WHEN "PMT"
                   PERFORM 6000-PROCESS-PAYMENT
               WHEN OTHER
                   PERFORM 9000-ERROR-HANDLER
           END-EVALUATE.

       2010-LOG-TXN-TYPE.
           ADD 1 TO WS-TXN-COUNT(TXN-TYPE-IDX)
           IF FUNCTION MOD(WS-TOTAL-TXN-COUNT, 10000) = 0
               DISPLAY "TXN Distribution: "
                   "DEP=" WS-TXN-COUNT(1) " "
                   "WTH=" WS-TXN-COUNT(2) " "
                   "XFR=" WS-TXN-COUNT(3) " "
                   "PMT=" WS-TXN-COUNT(4)
           END-IF.

This instrumentation reveals that deposits account for 45% of transactions, withdrawals 30%, transfers 15%, and payments 10%. This data drives the migration sequence — start with the highest-volume, simplest transaction type.

Phase 1: Migrate deposits (the simplest transaction type). Build the new Java deposit service. Run it in parallel with the COBOL deposit logic, comparing results:

// Parallel execution for validation
@Service
public class DepositService {

    @Autowired
    private CICSGateway cobolGateway;

    @Autowired
    private DepositRepository depositRepo;

    public DepositResult processDeposit(DepositRequest request) {
        // Execute BOTH old and new implementations
        DepositResult cobolResult = processViaCobol(request);
        DepositResult javaResult = processViaJava(request);

        // Compare results
        if (!cobolResult.equals(javaResult)) {
            log.error("MISMATCH: COBOL={} Java={} Txn={}",
                cobolResult, javaResult, request.getTxnId());
            // Use COBOL result (the trusted one) while investigating
            metrics.increment("strangler.mismatch.deposit");
            return cobolResult;
        }

        metrics.increment("strangler.match.deposit");
        return cobolResult; // Still using COBOL result
    }
}

Phase 2: Shadow mode. After the parallel comparison shows 100% match over two weeks (covering month-end, quarter-end, and various edge cases), switch to shadow mode: the Java service handles the request, but the COBOL system also runs as a validation check. Mismatches trigger alerts but don't affect the customer.

Phase 3: Cutover. After another two weeks of clean shadow mode, stop calling the COBOL deposit logic. The router sends all deposit transactions to the Java service. The COBOL deposit paragraphs become dead code.

Phase 4: Repeat for withdrawals, transfers, payments. Each transaction type follows the same instrument-parallel-shadow-cutover cycle. The entire migration takes 12-18 months, with zero big-bang risk.

Rollback Strategy

Every phase must have a rollback plan. The routing layer makes rollback trivial — flip the feature flag and all traffic returns to the COBOL system:

# Feature flags for strangler fig routing
routing:
  transactions:
    deposit:
      target: "java"          # "cobol" or "java"
      shadow_mode: false
      parallel_compare: false
    withdrawal:
      target: "cobol"         # Not yet migrated
      shadow_mode: false
      parallel_compare: false
    transfer:
      target: "cobol"
      shadow_mode: false
      parallel_compare: true   # In parallel comparison
    payment:
      target: "cobol"         # Not yet migrated
      shadow_mode: false
      parallel_compare: false

💡 The Key Insight: The Strangler Fig pattern never requires a big-bang cutover. At every point in the migration, rolling back to the old system is a configuration change, not a code change. This dramatically reduces risk compared to a complete rewrite-and-replace approach.

37.13 API Gateway Configuration

An API gateway sits between external consumers and your backend services (whether COBOL, Java, or hybrid). It handles cross-cutting concerns like authentication, rate limiting, routing, and protocol transformation.

Gateway Architecture for COBOL Modernization

┌──────────────┐     ┌──────────────────────────────┐
│  External    │     │        API GATEWAY            │
│  Consumers   │     │                                │
│              │     │  ┌──────────┐ ┌────────────┐  │
│  Mobile App  │────►│  │  Auth    │ │   Rate     │  │
│  Web Portal  │     │  │  (OAuth2)│ │   Limiter  │  │
│  Partner API │     │  └──────────┘ └────────────┘  │
│              │     │  ┌──────────┐ ┌────────────┐  │
│              │     │  │  Router  │ │  Transform │  │
│              │     │  │          │ │  JSON↔XML  │  │
│              │     │  └─────┬────┘ └────────────┘  │
│              │     └────────┼──────────────────────┘
│              │              │
│              │     ┌────────┼──────────────────────┐
│              │     │        ▼                       │
│              │     │  ┌──────────┐ ┌────────────┐  │
│              │     │  │  COBOL   │ │   JAVA     │  │
│              │     │  │  via     │ │   Micro-   │  │
│              │     │  │  z/OS    │ │   services │  │
│              │     │  │  Connect │ │            │  │
│              │     │  └──────────┘ └────────────┘  │
│              │     │     BACKEND SERVICES           │
│              │     └───────────────────────────────┘

Rate Limiting for COBOL Backends

COBOL/CICS systems have finite transaction capacity. Unlike cloud-native services that auto-scale, a CICS region has a fixed number of available threads. The API gateway must enforce rate limits to prevent overwhelming the mainframe:

# API gateway rate limiting configuration
rate_limits:
  global:
    requests_per_second: 500
    burst: 100
  per_consumer:
    mobile_app:
      requests_per_second: 200
      burst: 50
    partner_api:
      requests_per_second: 100
      burst: 20
    web_portal:
      requests_per_second: 200
      burst: 50
  per_endpoint:
    /api/v1/accounts/balance:
      requests_per_second: 300    # Read-only, high capacity
    /api/v1/accounts/transfer:
      requests_per_second: 50     # Write operation, limited
    /api/v1/claims/submit:
      requests_per_second: 100    # Moderate capacity

Request/Response Transformation

The gateway transforms between modern API conventions and COBOL's fixed-format data:

// Incoming REST request (modern format)
{
    "accountNumber": "1234567890",
    "transferAmount": 500.00,
    "fromAccount": "checking",
    "toAccount": "savings",
    "memo": "Monthly savings"
}

The gateway transforms this into a fixed-format COMMAREA:

Position  Length  Field                  Value
1-10      10     ACCT-NUMBER            1234567890
11-20     10     FROM-ACCT-TYPE         CHECKING
21-30     10     TO-ACCT-TYPE           SAVINGS
31-39      9     TRANSFER-AMT (COMP-3)  00050000{
40-79     40     MEMO-TEXT              Monthly savings

And transforms the COBOL response back to JSON:

// Outgoing REST response
{
    "status": "SUCCESS",
    "transactionId": "TXN2025111500234",
    "fromBalance": 3500.00,
    "toBalance": 12500.00,
    "timestamp": "2025-11-15T14:30:00Z"
}

Try It Yourself: Design an API gateway configuration for MedClaim's claim status API (from Section 37.11). Define the rate limits, request/response transformations, and error mapping. Consider what happens when the COBOL backend is unavailable — should the gateway return a cached response, a 503 error, or a degraded response with partial data?

37.14 Database Migration Step-by-Step

Migrating from VSAM to a relational database is one of the most common and most challenging aspects of COBOL modernization. This section provides a step-by-step walkthrough using GlobalBank's ACCT-MASTER VSAM file as the example.

Step 1: Analyze the Existing Data Model

Extract the VSAM record layout from the COBOL copybook:

      * CPY-ACCT-REC: Account master record layout
       01  ACCT-MASTER-REC.
           05 ACCT-NUMBER          PIC X(10).
           05 ACCT-TYPE            PIC X(3).
           05 ACCT-STATUS          PIC X.
           05 ACCT-OPEN-DATE       PIC X(10).
           05 ACCT-CLOSE-DATE      PIC X(10).
           05 ACCT-BALANCE         PIC S9(11)V99  COMP-3.
           05 ACCT-ANNUAL-RATE     PIC V9(6)      COMP-3.
           05 ACCT-COMPOUND-METHOD PIC X.
           05 ACCT-CUSTOMER-INFO.
              10 CUST-NAME         PIC X(40).
              10 CUST-SSN          PIC X(9).
              10 CUST-ADDRESS      PIC X(60).
              10 CUST-CITY         PIC X(25).
              10 CUST-STATE        PIC X(2).
              10 CUST-ZIP          PIC X(10).
           05 ACCT-BRANCH-CODE     PIC X(5).
           05 ACCT-LAST-TXN-DATE   PIC X(10).
           05 FILLER               PIC X(13).

Step 2: Design the Relational Schema

The flat VSAM record should be normalized into proper relational tables:

-- Separate customer data from account data (normalization)
CREATE TABLE CUSTOMERS (
    CUSTOMER_ID     INTEGER GENERATED ALWAYS AS IDENTITY,
    SSN             CHAR(9) NOT NULL,
    FULL_NAME       VARCHAR(40) NOT NULL,
    ADDRESS_LINE    VARCHAR(60),
    CITY            VARCHAR(25),
    STATE_CODE      CHAR(2),
    ZIP_CODE        VARCHAR(10),
    CREATED_DATE    DATE DEFAULT CURRENT_DATE,
    CONSTRAINT PK_CUSTOMERS PRIMARY KEY (CUSTOMER_ID),
    CONSTRAINT UK_CUST_SSN UNIQUE (SSN)
);

CREATE TABLE ACCOUNTS (
    ACCOUNT_NUMBER  CHAR(10) NOT NULL,
    CUSTOMER_ID     INTEGER NOT NULL,
    ACCOUNT_TYPE    CHAR(3) NOT NULL,
    STATUS          CHAR(1) NOT NULL DEFAULT 'A',
    OPEN_DATE       DATE NOT NULL,
    CLOSE_DATE      DATE,
    BALANCE         DECIMAL(13,2) NOT NULL DEFAULT 0,
    ANNUAL_RATE     DECIMAL(7,6) NOT NULL DEFAULT 0,
    COMPOUND_METHOD CHAR(1) NOT NULL DEFAULT 'D',
    BRANCH_CODE     CHAR(5),
    LAST_TXN_DATE   DATE,
    CONSTRAINT PK_ACCOUNTS PRIMARY KEY (ACCOUNT_NUMBER),
    CONSTRAINT FK_ACCT_CUST FOREIGN KEY (CUSTOMER_ID)
        REFERENCES CUSTOMERS (CUSTOMER_ID),
    CONSTRAINT CHK_ACCT_TYPE
        CHECK (ACCOUNT_TYPE IN ('CHK','SAV','CD','MMA')),
    CONSTRAINT CHK_STATUS
        CHECK (STATUS IN ('A','I','C','F'))
);

-- Indexes for common access patterns
CREATE INDEX IX_ACCT_CUST ON ACCOUNTS (CUSTOMER_ID);
CREATE INDEX IX_ACCT_TYPE_STATUS ON ACCOUNTS (ACCOUNT_TYPE, STATUS);
CREATE INDEX IX_ACCT_BRANCH ON ACCOUNTS (BRANCH_CODE);

Step 3: Build the Migration ETL

      *================================================================*
      * VSAM-TO-DB2: Extract VSAM records and load into DB2 tables.
      * Handles customer deduplication and data type conversion.
      *================================================================*
       IDENTIFICATION DIVISION.
       PROGRAM-ID. VSAM-TO-DB2.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
           COPY CPY-ACCT-REC.
           COPY SQLCA.
       01  WS-CUSTOMER-ID         PIC S9(9) COMP.
       01  WS-RECORDS-READ        PIC 9(9) COMP  VALUE 0.
       01  WS-CUSTS-INSERTED      PIC 9(9) COMP  VALUE 0.
       01  WS-ACCTS-INSERTED      PIC 9(9) COMP  VALUE 0.
       01  WS-ERRORS              PIC 9(9) COMP  VALUE 0.
       01  WS-COMMIT-INTERVAL     PIC 9(5) COMP  VALUE 5000.

       PROCEDURE DIVISION.
       0000-MAIN.
           OPEN INPUT ACCT-MASTER-FILE
           PERFORM 1000-READ-NEXT-RECORD
           PERFORM 2000-PROCESS-RECORD
               UNTIL END-OF-FILE
           PERFORM 8000-FINAL-COMMIT
           PERFORM 9000-DISPLAY-STATS
           CLOSE ACCT-MASTER-FILE
           STOP RUN.

       2000-PROCESS-RECORD.
      *    Step A: Check if customer already exists (by SSN)
           EXEC SQL
               SELECT CUSTOMER_ID
               INTO :WS-CUSTOMER-ID
               FROM CUSTOMERS
               WHERE SSN = :CUST-SSN
           END-EXEC

           EVALUATE SQLCODE
               WHEN 0
                   CONTINUE
               WHEN 100
                   PERFORM 2100-INSERT-CUSTOMER
               WHEN OTHER
                   PERFORM 9500-SQL-ERROR
           END-EVALUATE

      *    Step B: Insert account record
           EXEC SQL
               INSERT INTO ACCOUNTS
               (ACCOUNT_NUMBER, CUSTOMER_ID, ACCOUNT_TYPE,
                STATUS, OPEN_DATE, CLOSE_DATE, BALANCE,
                ANNUAL_RATE, COMPOUND_METHOD, BRANCH_CODE,
                LAST_TXN_DATE)
               VALUES
               (:ACCT-NUMBER, :WS-CUSTOMER-ID,
                :ACCT-TYPE, :ACCT-STATUS,
                :ACCT-OPEN-DATE, :ACCT-CLOSE-DATE,
                :ACCT-BALANCE, :ACCT-ANNUAL-RATE,
                :ACCT-COMPOUND-METHOD, :ACCT-BRANCH-CODE,
                :ACCT-LAST-TXN-DATE)
           END-EXEC

           IF SQLCODE = 0
               ADD 1 TO WS-ACCTS-INSERTED
           ELSE
               ADD 1 TO WS-ERRORS
               PERFORM 9500-SQL-ERROR
           END-IF

      *    Step C: Periodic commit
           IF FUNCTION MOD(WS-RECORDS-READ,
                           WS-COMMIT-INTERVAL) = 0
               EXEC SQL COMMIT END-EXEC
               DISPLAY "Committed at record: "
                   WS-RECORDS-READ
           END-IF

           PERFORM 1000-READ-NEXT-RECORD.

Step 4: Validate the Migration

After loading, run validation queries comparing VSAM data to DB2 data:

-- Record count validation
SELECT COUNT(*) AS DB2_COUNT FROM ACCOUNTS;
-- Compare against IDCAMS LISTCAT record count

-- Balance total validation
SELECT SUM(BALANCE) AS TOTAL_BALANCE FROM ACCOUNTS;
-- Compare against COBOL program summing VSAM balances

-- Sample record validation (spot check)
SELECT * FROM ACCOUNTS WHERE ACCOUNT_NUMBER = '1234567890';
-- Compare field-by-field against VSAM record

Step 5: Modify COBOL Programs

Once DB2 tables are populated and validated, modify COBOL programs to use SQL instead of VSAM I/O. This is typically the most labor-intensive step:

      * BEFORE: VSAM KSDS access
       READ ACCT-MASTER-FILE
           KEY IS ACCT-NUMBER
           INVALID KEY
               SET ACCT-NOT-FOUND TO TRUE
       END-READ

      * AFTER: DB2 access (with equivalent error handling)
       EXEC SQL
           SELECT ACCOUNT_NUMBER, ACCOUNT_TYPE, BALANCE,
                  STATUS, ANNUAL_RATE, COMPOUND_METHOD
           INTO :ACCT-NUMBER, :ACCT-TYPE, :ACCT-BALANCE,
                :ACCT-STATUS, :ACCT-ANNUAL-RATE,
                :ACCT-COMPOUND-METHOD
           FROM ACCOUNTS
           WHERE ACCOUNT_NUMBER = :WS-LOOKUP-KEY
       END-EXEC
       EVALUATE SQLCODE
           WHEN 0
               SET ACCT-FOUND TO TRUE
           WHEN 100
               SET ACCT-NOT-FOUND TO TRUE
           WHEN OTHER
               PERFORM 9500-SQL-ERROR
       END-EVALUATE

⚠️ Caution: Database migration is not just a technical exercise. It changes the operational model — VSAM datasets are managed by storage administrators, while DB2 tables are managed by database administrators. Backup and recovery procedures change. Monitoring tools change. On-call responsibilities change. Plan for the operational transition, not just the data migration.

37.15 Cloud Deployment Comparison: AWS vs. Azure vs. GCP

For organizations considering cloud deployment of COBOL workloads, the three major cloud providers offer different strengths.

Feature Comparison Matrix

Feature AWS Azure GCP
COBOL Runtime GnuCOBOL on EC2/ECS Micro Focus on Azure VMs GnuCOBOL on GCE/GKE
Mainframe Migration AWS Mainframe Modernization (Blu Age, Micro Focus) Azure Migrate for Mainframe (Astadia, Micro Focus) Google Cloud Dual Run (partnership with Kyndryl)
Container Support ECS, EKS (Kubernetes) AKS (Kubernetes) GKE (Kubernetes)
Database RDS (DB2 compatible via Aurora PostgreSQL) Azure SQL, Db2 on Azure Cloud SQL, AlloyDB
File Storage EFS (sequential files), S3 Azure Files, Blob Storage Filestore, Cloud Storage
Batch Processing AWS Batch, Step Functions Azure Batch Cloud Batch, Dataflow
Integration API Gateway, MQ API Management, Service Bus API Gateway, Pub/Sub
Cost Model Pay-per-use EC2, reserved instances Pay-per-use VMs, reserved Pay-per-use, committed use
Mainframe Expertise Moderate (Blu Age acquisition) Strong (Micro Focus partnership) Growing (Kyndryl partnership)

AWS Mainframe Modernization Example

# AWS CloudFormation for COBOL batch processing
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  CobolBatchJob:
    Type: AWS::Batch::JobDefinition
    Properties:
      JobDefinitionName: bal-calc-batch
      Type: container
      ContainerProperties:
        Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/cobol-batch:latest
        Vcpus: 2
        Memory: 4096
        Command:
          - "./bal-calc"
        MountPoints:
          - ContainerPath: /data/input
            SourceVolume: input-data
          - ContainerPath: /data/output
            SourceVolume: output-data
      RetryStrategy:
        Attempts: 2

  NightlyBatchSchedule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: "cron(0 23 * * ? *)"
      Targets:
        - Id: bal-calc-target
          Arn: !Ref BatchJobQueue

Azure Deployment Example

# Azure ARM template for COBOL microservice
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cobol-interest-calc
  namespace: globalbank
spec:
  replicas: 3
  selector:
    matchLabels:
      app: interest-calc
  template:
    spec:
      containers:
      - name: interest-calc
        image: globalbank.azurecr.io/cobol-interest:1.0
        resources:
          requests:
            memory: "128Mi"
            cpu: "250m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        env:
        - name: DB_CONNECTION
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: connection-string

Decision Framework

Choose AWS when:
  - You need Blu Age automated COBOL-to-Java conversion
  - You have existing AWS infrastructure
  - You need AWS Batch for mainframe-style batch processing

Choose Azure when:
  - You need Micro Focus Enterprise Server for CICS/JCL emulation
  - You have existing Microsoft enterprise agreements
  - You need tight integration with Azure DevOps

Choose GCP when:
  - You need BigQuery for analytics on migrated data
  - You prioritize Kubernetes-native deployment (GKE)
  - You need Google's AI/ML services for code analysis

37.16 Risk Assessment Frameworks

Beyond the risk matrix in Section 37.9, a structured risk assessment framework helps quantify modernization risk and make data-driven decisions.

The COBOL Modernization Risk Score (CMRS)

Assign a risk score (1-5) to each factor and calculate a weighted total:

MODERNIZATION RISK ASSESSMENT
Program: TXN-PROC
Date: 2025-11-15
Assessor: Priya Kapoor

Factor                    Weight  Score  Weighted
─────────────────────────────────────────────────
Code complexity (CC max)   0.15    4      0.60
  (CC=47 in main paragraph)
Lines of code              0.10    3      0.30
  (8,200 lines)
Documentation quality      0.15    2      0.30
  (minimal comments, no header)
Test coverage              0.20    1      0.20
  (no unit tests exist)
External dependencies      0.15    3      0.45
  (3 subprogram calls, 2 VSAM files, 1 DB2 table)
Business criticality       0.10    5      0.50
  (processes all transactions — highest criticality)
Domain expertise available 0.15    3      0.45
  (Maria Chen knows it; Derek is learning)
─────────────────────────────────────────────────
TOTAL RISK SCORE:                         2.80

RISK INTERPRETATION:
  1.0 - 1.5:  Low risk — modernize aggressively
  1.6 - 2.5:  Moderate risk — proceed with caution
  2.6 - 3.5:  High risk — wrap/extend preferred over replace
  3.6 - 5.0:  Very high risk — maintain and wrap only

TXN-PROC scores 2.80 — high risk. Priya's recommendation: wrap as an API (Strategy 2) rather than rewrite. The lack of test coverage (score 1 = no tests) is the biggest risk driver. Before any code changes, build a test suite (Chapter 34).

Pre-Modernization Readiness Checklist

Before beginning any modernization effort, verify these prerequisites:

MODERNIZATION READINESS CHECKLIST
═══════════════════════════════════

DOCUMENTATION:
[ ] Business rules documented (or extractable from code)
[ ] Data dictionary exists for all files and databases
[ ] Program dependencies mapped (who calls whom)
[ ] Integration points documented (APIs, files, queues)

TESTING:
[ ] Unit tests exist for core business logic
[ ] Integration test suite exists
[ ] Regression test baseline established
[ ] Test data management process in place

PEOPLE:
[ ] COBOL expertise available throughout transition
[ ] Target technology skills available (Java, cloud, etc.)
[ ] Business SMEs identified and committed
[ ] Change management plan for operations team

INFRASTRUCTURE:
[ ] Target environment provisioned (cloud, on-prem)
[ ] CI/CD pipeline exists or planned
[ ] Monitoring and alerting for new platform
[ ] Disaster recovery plan for hybrid operation

GOVERNANCE:
[ ] Rollback plan for each migration phase
[ ] Success criteria defined and measurable
[ ] Regulatory compliance verified for target platform
[ ] Data residency requirements met

📊 By the Numbers: Industry studies of mainframe modernization projects report the following success rates by strategy: Maintain (95%), Wrap (85%), Extend (70%), Re-Architect (55%), Replace (40%). The inverse correlation between ambition and success rate is stark. This data does not mean replacement is always wrong — but it does mean that replacement projects require exceptional planning, discipline, and commitment to succeed.

37.17 MedClaim Case Study: Building a Hybrid Architecture

While the GlobalBank case study focused on wrapping existing COBOL as APIs, MedClaim's modernization took a different path — a hybrid architecture where new capabilities were built in Python and Java while the COBOL core remained for adjudication processing.

The Business Driver

MedClaim's provider network demanded three new capabilities that the existing COBOL system could not deliver:

  1. Real-time claim status notifications — push notifications when a claim status changes
  2. Predictive claim analytics — machine learning models to flag potentially fraudulent claims
  3. Provider self-service portal — a web application for providers to submit claims electronically and check status

None of these capabilities required changing the core adjudication logic. They required extending the system with modern interfaces and capabilities.

The Hybrid Architecture

┌───────────────────────────────────────────────────────┐
│                    NEW SERVICES                        │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐  │
│  │ Notification │ │  Fraud       │ │  Provider    │  │
│  │ Service      │ │  Detection   │ │  Portal      │  │
│  │ (Java/Kafka) │ │  (Python/ML) │ │  (React/     │  │
│  │              │ │              │ │   Spring)    │  │
│  └──────┬───────┘ └──────┬───────┘ └──────┬───────┘  │
│         │                │                │           │
│  ┌──────┴────────────────┴────────────────┴───────┐  │
│  │              EVENT BUS (Kafka)                   │  │
│  └──────────────────────┬──────────────────────────┘  │
│                         │                              │
├─────────────────────────┼──────────────────────────────┤
│                         │     INTEGRATION LAYER        │
│  ┌──────────────────────┴──────────────────────────┐  │
│  │         API Gateway + Change Data Capture        │  │
│  └──────────────────────┬──────────────────────────┘  │
│                         │                              │
├─────────────────────────┼──────────────────────────────┤
│                         │     COBOL CORE               │
│  ┌──────────────────────┴──────────────────────────┐  │
│  │     CLM-ADJUD     CLM-STATUS     CLM-ELIG       │  │
│  │     (CICS/DB2 — unchanged COBOL programs)       │  │
│  └─────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────┘

Change Data Capture (CDC)

The key integration technology was Change Data Capture — monitoring the DB2 CLAIMS table for changes and publishing events to the Kafka event bus. When CLM-ADJUD updates a claim's status from "IN_REVIEW" to "PAID," the CDC listener captures the change and publishes an event:

{
    "eventType": "CLAIM_STATUS_CHANGED",
    "timestamp": "2025-11-15T14:30:00Z",
    "claimNumber": "CLM2025001234",
    "previousStatus": "IN_REVIEW",
    "newStatus": "PAID",
    "paidAmount": 1250.00,
    "providerId": "PRV00100",
    "memberId": "MEM12345"
}

The Notification Service subscribes to this event and sends push notifications. The Fraud Detection service subscribes to CLAIM_SUBMITTED events and runs the ML model before adjudication begins. The Provider Portal subscribes to status changes for its providers.

Crucially, none of this required changing a single line of COBOL. The CLM-ADJUD program continues to process claims exactly as it always has. The new capabilities are built around the existing system, not inside it.

Results After Six Months

Metric Before After
Claim status call volume 2,400/day 850/day (65% reduction)
Fraud detection rate 2.1% (manual review) 4.8% (ML-assisted)
Provider claim submission 100% paper/fax 60% electronic
Time to detect status change 24 hours (next-day report) < 30 seconds (push notification)
COBOL code changes required 0 lines

James Okafor summarized the approach: "We did not modernize the COBOL. We modernized around the COBOL. The adjudication engine is the heart of our system — it works correctly, it is well-tested, and it processes $2 billion in claims per year. We would be foolish to rewrite it. Instead, we gave it a modern nervous system."

Lessons Learned

Sarah Kim documented the key lessons from MedClaim's hybrid approach:

  1. CDC is the bridge: Change Data Capture allowed the new services to react to COBOL's actions without modifying the COBOL. This is the lowest-risk integration pattern for legacy systems.

  2. Event-driven architecture decouples: By using Kafka as an event bus, the new services don't need to know about COBOL at all — they consume events. If the COBOL is eventually replaced, the events still flow; only the producer changes.

  3. Start with read-only capabilities: The first three capabilities (notifications, analytics, portal status view) were all read-only — they consumed data from the COBOL system but didn't write back. This eliminated the risk of the new services corrupting the core system.

  4. Plan for the write path: The next phase — electronic claim submission through the Provider Portal — requires writing back to the COBOL system. This is higher risk and requires careful API design, validation, and testing.

  5. Skills matter: The hybrid approach requires developers who understand both the legacy COBOL system and the modern technology stack. MedClaim invested in cross-training: Tomás Rivera learned Java/Spring Boot, while two new Java developers spent a month learning COBOL and DB2 with James Okafor. This cross-pollination of skills was essential for effective integration.

37.18 Testing the Modernized System

Modernization introduces a new category of testing concern: verifying that the old and new systems produce consistent results. This is especially challenging when the two systems use different data types, different rounding rules, or different error handling conventions.

Contract Testing for API Wrappers

When COBOL is exposed through an API wrapper, contract tests verify that the API contract remains stable:

# Contract test for GlobalBank balance inquiry API
import requests
import pytest

BASE_URL = "https://api.globalbank.com/v1"

def test_balance_inquiry_success():
    """Verify balance inquiry returns expected schema."""
    response = requests.get(
        f"{BASE_URL}/accounts/1234567890/balance",
        headers={"Authorization": "Bearer test-token"}
    )
    assert response.status_code == 200
    data = response.json()
    assert "accountBalance" in data
    assert "accountStatus" in data
    assert isinstance(data["accountBalance"], (int, float))
    assert data["accountStatus"] in ["A", "I", "C", "F"]

def test_balance_inquiry_not_found():
    """Verify 404 for nonexistent account."""
    response = requests.get(
        f"{BASE_URL}/accounts/9999999999/balance",
        headers={"Authorization": "Bearer test-token"}
    )
    assert response.status_code == 404

def test_balance_inquiry_invalid_format():
    """Verify 400 for malformed account number."""
    response = requests.get(
        f"{BASE_URL}/accounts/ABC/balance",
        headers={"Authorization": "Bearer test-token"}
    )
    assert response.status_code == 400

Parallel Running Validation

During the Strangler Fig migration, parallel running compares results from the old and new systems:

      *================================================================*
      * PARALLEL-VALIDATE: Compare COBOL and Java results
      * Input: COBOL output file and Java output file
      * Output: Comparison report with mismatches
      *================================================================*
       IDENTIFICATION DIVISION.
       PROGRAM-ID. PARALLEL-VALIDATE.

       DATA DIVISION.
       FILE SECTION.
       FD  COBOL-OUTPUT.
       01  COBOL-REC            PIC X(200).
       FD  JAVA-OUTPUT.
       01  JAVA-REC             PIC X(200).
       FD  MISMATCH-REPORT.
       01  MISMATCH-REC         PIC X(200).

       WORKING-STORAGE SECTION.
       01  WS-RECORDS-COMPARED   PIC 9(9)  VALUE 0.
       01  WS-MISMATCHES         PIC 9(9)  VALUE 0.
       01  WS-MATCH-PCT          PIC 9(3)V99.

       PROCEDURE DIVISION.
       0000-MAIN.
           OPEN INPUT COBOL-OUTPUT JAVA-OUTPUT
           OPEN OUTPUT MISMATCH-REPORT
           PERFORM 1000-READ-BOTH
           PERFORM 2000-COMPARE
               UNTIL END-OF-COBOL OR END-OF-JAVA
           PERFORM 3000-REPORT-SUMMARY
           CLOSE COBOL-OUTPUT JAVA-OUTPUT MISMATCH-REPORT
           STOP RUN.

       2000-COMPARE.
           ADD 1 TO WS-RECORDS-COMPARED
           IF COBOL-REC NOT = JAVA-REC
               ADD 1 TO WS-MISMATCHES
               PERFORM 2100-LOG-MISMATCH
           END-IF
           PERFORM 1000-READ-BOTH.

       3000-REPORT-SUMMARY.
           COMPUTE WS-MATCH-PCT =
               (1 - (WS-MISMATCHES / WS-RECORDS-COMPARED))
               * 100
           DISPLAY "Records Compared: " WS-RECORDS-COMPARED
           DISPLAY "Mismatches:       " WS-MISMATCHES
           DISPLAY "Match Rate:       " WS-MATCH-PCT "%"
           IF WS-MISMATCHES > 0
               DISPLAY "*** MISMATCHES DETECTED ***"
               DISPLAY "Review MISMATCH-REPORT for details"
               MOVE 8 TO RETURN-CODE
           ELSE
               DISPLAY "ALL RECORDS MATCH"
           END-IF.

This validation program runs nightly during the parallel phase, comparing transaction-by-transaction output from both systems. A match rate below 100% triggers investigation before proceeding with cutover.

Common Sources of Mismatch

When parallel running reveals mismatches between COBOL and Java implementations, the root causes typically fall into these categories:

Category Example Resolution
Rounding differences COBOL rounds COMP-3 to 2 decimal places; Java uses IEEE 754 floating point Use Java BigDecimal with ROUND_HALF_UP to match COBOL behavior
Date handling COBOL stores dates as PIC X(10) strings; Java uses LocalDate Ensure consistent timezone handling and date format parsing
Null/empty handling COBOL treats spaces as valid data; Java distinguishes null from empty string Define explicit mapping rules for space-to-null conversion
Sign handling COBOL PIC S9 stores sign in the zone nibble; Java uses negative numbers Verify sign extraction during COMMAREA parsing
Truncation behavior COBOL silently truncates on MOVE; Java throws exceptions on overflow Add explicit length checks in Java to match COBOL truncation behavior

⚠️ Caution: The most insidious mismatches are the ones that appear correct most of the time but fail on edge cases — a rounding difference that manifests only on amounts ending in exactly 5 mills, a date calculation that differs only on February 29 of leap years, a sign issue that appears only for negative balances. Parallel running must cover month-end, quarter-end, year-end, and leap year scenarios to catch these edge cases.

⚖️ Debate: Hybrid vs. Full Migration: The hybrid architecture has a hidden cost: operational complexity. MedClaim now runs three technology stacks (COBOL/DB2, Java/Spring, Python/ML) with three different deployment pipelines, three different monitoring tools, and three different skill sets. Each technology adds operational overhead. Proponents argue this is a reasonable price for risk reduction. Critics argue it creates a "worst of both worlds" scenario where the organization must maintain all the legacy costs plus the new platform costs. The right answer depends on the organization's ability to manage multi-platform complexity — a capability that varies widely across enterprises.

37.18 Summary

Migration and modernization is not a one-size-fits-all proposition. The right strategy depends on your system's characteristics, your organization's needs, and your tolerance for risk.

The key concepts from this chapter:

  • The modernization spectrum ranges from maintain (lowest risk, lowest cost) through wrap, extend, and re-architect, to replace (highest risk, highest cost).
  • API wrapping is the most common and most successful modernization approach — expose existing COBOL as REST APIs without changing the code.
  • Database migration (IMS to DB2, VSAM to DB2) enables SQL access but requires careful data modeling and program changes.
  • Cloud deployment is viable for COBOL through recompilation (GnuCOBOL), managed runtimes (Micro Focus), or mainframe-as-a-service (IBM).
  • Containerized COBOL enables microservice architecture — a COBOL program doing one thing well fits the microservice pattern.
  • Rewriting is the riskiest strategy and should be a last resort. The Strangler Fig pattern mitigates rewrite risk through incremental replacement.
  • AI-assisted analysis helps understand legacy code but cannot replace human judgment in modernization decisions.
  • Risk assessment must weigh the risks of modernization against the risks of doing nothing — talent gaps, rising costs, and capability limitations.

Priya Kapoor's approach — different strategies for different subsystems, executed incrementally with clear value delivery at each phase — is the gold standard. Modernization is not a destination; it's a journey. And the first step is understanding where you are on the spectrum.

This concludes Part VII: Testing, Debugging, and Quality. You now have the skills to test COBOL programs (Chapter 34), review them for quality (Chapter 35), tune their performance (Chapter 36), and plan their future (Chapter 37). In Part VIII, we'll apply these skills to enterprise-scale patterns and architectural decisions.