> "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 This Chapter
- 37.1 The Modernization Spectrum
- 37.2 API Wrapping — Exposing COBOL as Services
- 37.3 Screen Scraping and Terminal Emulation
- 37.4 Database Migration
- 37.5 Cloud Deployment — COBOL on AWS/Azure/GCP
- 37.6 Microservices with COBOL
- 37.7 Rewriting Strategies
- 37.8 AI-Assisted Code Analysis
- 37.9 Risk Assessment
- 37.10 GlobalBank Case Study: Evaluating Modernization Options
- 37.11 MedClaim Case Study: Wrapping Claims Processing as a Microservice
- 37.12 The Strangler Fig Pattern in Detail
- 37.13 API Gateway Configuration
- 37.14 Database Migration Step-by-Step
- 37.15 Cloud Deployment Comparison: AWS vs. Azure vs. GCP
- 37.16 Risk Assessment Frameworks
- 37.17 MedClaim Case Study: Building a Hybrid Architecture
- 37.18 Testing the Modernized System
- 37.18 Summary
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
subprocessmodule 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
- The business logic has fundamentally changed — the existing code implements rules that no longer apply
- The architecture prevents essential capabilities — real-time processing, horizontal scaling, cloud-native deployment
- The codebase is unmaintainable — no tests, no documentation, no one who understands it
- 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
- Code summarization: Generate natural-language descriptions of what a COBOL paragraph does
- Business rule extraction: Identify the business rules embedded in code (e.g., "claims over $50,000 require manual review")
- Dependency mapping: Trace data flows across programs, copybooks, and files
- Dead code identification: Identify unreachable code paths with greater accuracy than static analysis alone
- Documentation generation: Create technical documentation from undocumented code
- Translation assistance: Help translate COBOL to modern languages with better code quality than fully automated tools
What AI Cannot Do (Yet)
- 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
- Validate correctness: AI can translate code but cannot guarantee the translation preserves all edge-case behavior
- 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:
- Real-time claim status notifications — push notifications when a claim status changes
- Predictive claim analytics — machine learning models to flag potentially fraudulent claims
- 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:
-
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.
-
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.
-
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.
-
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.
-
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.