The conversion completed in three months. The Java application compiled, passed basic smoke tests, and was pronounced ready for deployment. Six months into production, the platform team noticed something troubling: nobody on the Java team could maintain the code. The reviews took twice as long as normal Java reviews. The bug reports referenced concepts, WORKING-STORAGE, PROCEDURE DIVISION logic, packed decimal field conventions, that Java developers without mainframe experience could not follow. The application had been converted from COBOL to Java, but the resulting Java thought, structured, and behaved like COBOL. It was Jobol.
Jobol (sometimes styled JOBOL) is the portmanteau for Java code that retains COBOL’s procedural semantics without embracing Java’s object-oriented capabilities. Jason Bloomberg, founder of analyst firm Intellyx, defined it directly: Jobol is technically valid Java code that leaves the original software architecture in place and preserves COBOL semantics, requiring developers to treat the Java as though it were COBOL. The critical consequence follows immediately: an enterprise that converts COBOL to Jobol has not freed itself from the need for COBOL expertise. It has simply moved that expertise requirement to a language that does not natively support the COBOL patterns the code contains. It has created a system that is harder to maintain than the original COBOL and provides none of the expected Java benefits.
Dependency Graph Before Domain Design
SMART TS XL maps every PERFORM and CALL dependency so the Java service layer reflects the actual coupling structure, not the COBOL class structure.
FIND OUT MORE…What Jobol Actually Looks Like
Before diagnosing the causes, the pattern needs to be visible. Here is the same business logic, a customer credit check, in three forms: the original COBOL, the Jobol that automated converters typically produce, and the idiomatic Java that a skilled modernization produces.
The original COBOL:
cobol
PERFORM-CREDIT-CHECK.
MOVE WS-CUST-ID TO DB-CUST-KEY
READ CUSTOMER-FILE KEY IS DB-CUST-KEY
INVALID KEY
MOVE 'NOT FOUND' TO WS-STATUS
PERFORM HANDLE-MISSING-CUSTOMER
END-READ
IF CUST-CREDIT-SCORE < 650
MOVE 'DECLINED' TO WS-CREDIT-RESULT
MOVE 'Score below threshold' TO WS-DECLINE-REASON
ELSE IF CUST-OUTSTANDING-BALANCE > CUST-CREDIT-LIMIT
MOVE 'DECLINED' TO WS-CREDIT-RESULT
MOVE 'Balance exceeds limit' TO WS-DECLINE-REASON
ELSE
MOVE 'APPROVED' TO WS-CREDIT-RESULT
END-IF.
The Jobol produced by a line-by-line converter:
java
// Jobol: procedural COBOL logic mapped directly to Java
// No OOP, no encapsulation, static global state, COBOL naming conventions
public class CreditCheckProgram {
static String WS_CUST_ID;
static String DB_CUST_KEY;
static String WS_STATUS;
static String WS_CREDIT_RESULT;
static String WS_DECLINE_REASON;
static int CUST_CREDIT_SCORE;
static double CUST_OUTSTANDING_BALANCE;
static double CUST_CREDIT_LIMIT;
public static void PERFORM_CREDIT_CHECK() {
DB_CUST_KEY = WS_CUST_ID;
Customer customer = CustomerFile.read(DB_CUST_KEY);
if (customer == null) {
WS_STATUS = "NOT FOUND";
HANDLE_MISSING_CUSTOMER();
return;
}
CUST_CREDIT_SCORE = customer.getCreditScore();
CUST_OUTSTANDING_BALANCE = customer.getOutstandingBalance();
CUST_CREDIT_LIMIT = customer.getCreditLimit();
if (CUST_CREDIT_SCORE < 650) {
WS_CREDIT_RESULT = "DECLINED";
WS_DECLINE_REASON = "Score below threshold";
} else if (CUST_OUTSTANDING_BALANCE > CUST_CREDIT_LIMIT) {
WS_CREDIT_RESULT = "DECLINED";
WS_DECLINE_REASON = "Balance exceeds limit";
} else {
WS_CREDIT_RESULT = "APPROVED";
}
}
}
The idiomatic Java a skilled modernization produces:
java
// Idiomatic Java: domain model, encapsulation, value objects, Spring service
@Service
public class CreditCheckService {
private final CustomerRepository customerRepository;
public CreditCheckService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public CreditDecision check(CustomerId customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
if (customer.creditScore() < CreditThresholds.MINIMUM_SCORE) {
return CreditDecision.declined("Score below threshold");
}
if (customer.outstandingBalance().compareTo(customer.creditLimit()) > 0) {
return CreditDecision.declined("Balance exceeds limit");
}
return CreditDecision.approved();
}
}
The Jobol version is 32 lines of static global state, procedurally structured, COBOL-named, and impossible to unit test without initializing the global class state first. The idiomatic version is 20 lines, injectable, testable, and immediately readable by any Java developer without COBOL background. Both implement the same business rules. Only one is maintainable by the team the modernization was supposed to enable.
The Six Jobol Patterns and Why Converters Produce Them
Pattern 1: Global Static State Replacing WORKING-STORAGE
COBOL’s WORKING-STORAGE SECTION holds program-level variables that persist across PERFORM calls. It is the program’s shared mutable state, every paragraph reads and writes the same WS variables. Automated converters map this directly to Java static fields on a single class, preserving the shared-state semantics while violating every Java encapsulation principle.
Why converters do this: WORKING-STORAGE to static field mapping is a mechanical one-to-one transformation. The converter does not understand that Java’s encapsulation model replaces program-level shared state with instance variables, method parameters, and return values. It converts what it sees without understanding why the original structure existed.
The consequence: Static state is untestable in isolation, thread-unsafe without explicit synchronization, and structurally identical to the COBOL it replaced. Any developer who needs to add concurrency to the system must first untangle the static state, work that was deferred from the conversion to every subsequent maintainer.
The fix: Map WORKING-STORAGE variables to instance fields, method parameters, or return values based on their scope of use. Variables used only within one paragraph become local variables. Variables passed between paragraphs become method parameters or return values. Variables that truly represent shared object state become instance fields on a properly encapsulated domain class.
Pattern 2: PERFORM Chains Becoming Long Static Methods
COBOL programs are organized as PERFORM chains, the main procedure PERFORMs a sequence of named paragraphs, each of which may PERFORM others. Automated converters translate each paragraph into a static method and each PERFORM into a static method call. The result is a single class with dozens of static methods calling each other through a call graph that mirrors the COBOL PERFORM structure.
java
// Jobol: PERFORM chain mapped to static method calls
public class ProcessOrderProgram {
public static void main() {
validateOrder();
checkInventory();
calculateTotal();
applyDiscounts();
processPayment();
generateConfirmation();
updateInventory();
}
public static void validateOrder() { /* ... */ }
public static void checkInventory() { /* ... */ }
// ... 40 more static methods
}
Why converters do this: The structural mapping from COBOL paragraph to Java method is direct. The converter correctly identifies that PERFORM means “call this code unit.” What it misses is that Java’s object model organizes code around domain entities, not around processing steps.
The fix: Identify the domain concepts embedded in the PERFORM chain. validateOrder, calculateTotal, and applyDiscounts are operations on an Order. processPayment is an operation on a payment context. checkInventory and updateInventory are operations on Inventory. The refactoring distributes the static methods to the domain classes they belong to, replacing a long procedural call chain with a short orchestration method that delegates to appropriate domain objects.
Pattern 3: Numeric Types Without Precision Handling
COBOL’s packed decimal (COMP-3) fields have exact decimal arithmetic. Converters that map COMP-3 to Java double or float introduce systematic floating-point rounding errors that accumulate across millions of financial transactions.
java
// Jobol: COMP-3 mapped to double (wrong)
static double WS_TRANSACTION_AMOUNT; // Was PIC S9(9)V99 COMP-3
static double WS_RUNNING_TOTAL;
// After 10M additions of 0.1:
// Expected: 1,000,000.00
// Actual: 1,000,000.0000000002 (floating point accumulation)
java
// Idiomatic Java: COMP-3 mapped to BigDecimal (correct)
private BigDecimal transactionAmount; // Preserves decimal precision
private BigDecimal runningTotal;
// After 10M additions of 0.1:
// Result: exactly 1,000,000.00
Why converters do this: double is Java’s default numeric type. A converter that does not specifically understand COMP-3 semantics will default to the most common Java numeric type, which is the wrong choice for exact decimal arithmetic.
The fix: Every COMP-3 field maps to BigDecimal. Every numeric calculation that involves COMP-3 values uses BigDecimal arithmetic. The scale() of the BigDecimal matches the implied decimal places in the PIC clause: PIC S9(9)V99 has 2 implied decimal places, mapping to BigDecimal with scale 2.
Pattern 4: EBCDIC Strings and Byte-Array Relics
COBOL processes character data as fixed-length padded strings in EBCDIC encoding. Converters often produce Java code that preserves the fixed-length, space-padded string semantics even after conversion to UTF-8, resulting in Java that handles strings like a 1970s mainframe programmer.
java
// Jobol: preserves COBOL fixed-length string handling
static char[] WS_CUSTOMER_NAME = new char[40]; // Was PIC X(40)
static char[] WS_CUSTOMER_ADDR = new char[80]; // Was PIC X(80)
// COBOL-style space-padding comparison
if (new String(WS_CUSTOMER_NAME).trim().equals("SMITH")) { ... }
// COBOL-style string concatenation with padding
System.arraycopy(name.toCharArray(), 0, WS_CUSTOMER_NAME, 0,
Math.min(name.length(), 40));
java
// Idiomatic Java: proper String types and domain objects
private String customerName; // Trim on read, no padding
private String customerAddress;
// Clean comparison
if ("Smith".equalsIgnoreCase(customer.name())) { ... }
Why converters do this: The COBOL PIC X(40) definition has a length that the converter preserves as a Java array size. The EBCDIC-to-UTF-8 conversion may be handled, but the fixed-length semantics are carried across unnecessarily.
The fix: Map PIC X(n) fields to String objects, trimming on read and removing fixed-length constraints. The only context where length constraints matter is at persistence boundaries (database column lengths, file format positions), not in the domain model.
Pattern 5: Error Handling via Status Codes
COBOL communicates error conditions through status codes: FILE STATUS, SQLCODE, RETURN-CODE. Converters preserve this pattern in Java, producing code that checks integer status codes after every operation rather than using Java’s exception model.
java
// Jobol: COBOL status code pattern in Java
static int WS_FILE_STATUS;
static int WS_SQL_CODE;
CustomerFile.read(DB_CUST_KEY);
WS_FILE_STATUS = CustomerFile.getLastStatus();
if (WS_FILE_STATUS == 10) {
// Not found
} else if (WS_FILE_STATUS == 35) {
// File not available
} else if (WS_FILE_STATUS != 0) {
// Unknown error
}
java
// Idiomatic Java: exceptions for error conditions
try {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
// process customer
} catch (DatabaseUnavailableException e) {
log.error("Database unavailable during credit check", e);
throw new CreditCheckServiceException("Service temporarily unavailable", e);
}
Why converters do this: Status code checking is the COBOL pattern; converting it to a Java method that returns or sets a status field is a mechanical transformation. Recognizing that Java exceptions are the idiomatic equivalent requires architectural judgment, not mechanical translation.
The fix: Map COBOL error conditions to Java exceptions. FILE STATUS 10 (record not found) becomes Optional.empty() or a domain-specific exception. SQLCODE errors become typed database exceptions. The Java code structure reflects intent, try/catch for expected failure modes, unchecked exceptions for unexpected ones.
Pattern 6: Monolithic Class Structure
COBOL programs are monolithic by design, a single program handles an entire business function. Converters create a single Java class per COBOL program, producing Java classes that may be thousands of lines long and handle multiple responsibilities that object-oriented design would distribute across several focused classes.
Why converters do this: One COBOL program maps to one Java class, the converter’s unit of translation is the program. The internal structure of the program (its paragraphs, sections, PERFORM chains) becomes the internal structure of the class without decomposition into smaller classes.
The fix: Apply the Single Responsibility Principle to each converted class. A 3,000-line Jobol class that validates input, reads from a database, applies business rules, formats output, and writes to a file should become: an input validator, a repository, a domain service, a formatter, and a writer, each testable in isolation, each focused on one responsibility.
The Structural Analysis Prerequisite
Every Jobol pattern has a corresponding refactoring. But the refactoring cannot be applied by the converter, it requires understanding what the program does before deciding how to restructure it. That understanding is exactly what structural analysis of the source COBOL provides before any conversion tool runs.
Specifically, structural analysis produces the inputs that conversion decisions require:
Business logic inventory. The complete enumeration of EVALUATE conditions, IF branches, and COMPUTE formulas in each program, what the program decides, not just how it is structured. This inventory is the specification that the refactored Java must match. A program with 847 EVALUATE conditions requires a target class structure that accommodates 847 decision points; the converter’s output is the starting draft, not the finished product.
Dependency graph. Which programs call which other programs. The PERFORM chain that spans multiple COBOL programs becomes a service call graph in Java. The dependency graph determines which programs can be refactored independently (few inbound dependencies) and which must be refactored as coordinated groups (many shared dependencies through copybooks or shared data).
Dead code identification. Paragraphs and sections that are never PERFORMed from any production execution path are dead code, they should not appear in the converted Java at all. Converters that include dead code inflate the converted codebase and the subsequent refactoring effort.
Data model recovery. The VSAM FD entries, DB2 table definitions, and copybook structures that define the program’s data. Each COBOL data structure maps to one or more Java domain objects; the mapping is determined by the data model, not by the field names in WORKING-STORAGE.
| Analysis Input | Conversion Decision It Drives |
|---|---|
| EVALUATE/IF branch count | Domain class decomposition, how many decision cases each service handles |
| Inbound dependency count | Refactoring sequence, fewest-dependency programs first |
| Dead code percentage | Scope reduction, exclude unreachable code from conversion |
| COMP-3 fields | BigDecimal mapping for every numeric field |
| PIC X field lengths | String type mapping and persistence-layer constraints |
| PERFORM graph structure | Service layer decomposition, PERFORM chains become domain service calls |
| FILE STATUS usage | Exception model design, which status codes map to which exception types |
What the Conversion Should Produce Instead
The alternative to Jobol is a conversion process that uses the COBOL as a specification rather than as a template. The COBOL program knows what the business logic is, the Java program should express that logic in Java’s idiom.
The conversion principle: Convert the intent, not the syntax. The COBOL
IF CUST-CREDIT-SCORE < 650 MOVE 'DECLINED' TO WS-CREDIT-RESULTsays “a customer with a credit score below 650 is declined.” The Java should say the same thing in Java’s language:if (customer.creditScore() < CreditThresholds.MINIMUM_SCORE) return CreditDecision.declined(...). Notif (CUST_CREDIT_SCORE < 650) { WS_CREDIT_RESULT = "DECLINED"; }.
The practical conversion workflow that produces idiomatic Java:
Step 1: Structural analysis of the COBOL source, dependency graph, business logic inventory, data model recovery, dead code identification. This step produces the specification. Nothing is converted until this step is complete.
Step 2: Domain model design, identify the business entities (Customer, Account, Transaction, Policy) that the COBOL programs operate on. These become Java domain classes. The WORKING-STORAGE variables that represent entity state become instance fields on the appropriate domain class.
Step 3: Service layer design, identify the business operations (credit check, payment processing, balance calculation) that the COBOL paragraphs and sections implement. These become Java service methods on the appropriate domain service class. The PERFORM chain that sequences paragraphs becomes the orchestration logic of the service.
Step 4: Automated conversion as a draft generator, use the conversion tool to produce a starting-point Java draft for each program, informed by the domain model designed in Steps 2 and 3. Review the draft against the business logic inventory to verify that every decision branch is represented.
Step 5: Systematic refactoring against the Jobol patterns, apply the six anti-pattern fixes: replace static state with instance state, distribute static methods to domain classes, replace status codes with exceptions, replace double with BigDecimal, replace fixed-length arrays with String, decompose monolithic classes into single-responsibility services.
Step 6: Behavioral equivalence testing, run the refactored Java against the characterization test corpus generated from the COBOL analysis. Every EVALUATE condition and IF branch should have at least one test case that exercises it and verifies equivalence with the COBOL output.
How SMART TS XL Prevents Jobol at the Source
The structural analysis prerequisite for Jobol prevention, the dependency graph, business logic inventory, dead code identification, and data model recovery, is exactly what SMART TS XL’s analysis of the COBOL source provides before any conversion tool runs.
SMART TS XL’s static code analysis enumerates every EVALUATE condition, every IF branch, every COMPUTE formula, and every PERFORM relationship in the COBOL source, the complete business logic inventory that the refactored Java must faithfully represent. When the conversion tool produces Jobol, the business logic inventory is the specification against which the refactoring is validated: every condition in the inventory must appear in the Java, in the appropriate domain class, expressed in Java’s idiom.
The application dependency mapping produces the PERFORM graph and program call graph that determine the Java service layer structure. Programs that call each other frequently belong in the same bounded context. Programs that are called by many others are the shared services that must be designed carefully to support all their consumers in the Java architecture. The dependency graph is the input to the service layer design step, converting COBOL’s implicit coupling structure into Java’s explicit dependency injection structure.
The legacy modernization analysis identifies dead code, paragraphs and sections never reached from production execution paths, that should be excluded from the conversion scope entirely. Converting dead COBOL to dead Jobol and then refactoring dead Jobol wastes effort that the dead code identification step eliminates before conversion starts.
The impact analysis capability validates the refactoring sequence: when a COBOL program’s interface changes during the refactoring from Jobol to idiomatic Java, the impact analysis identifies every other program whose Java equivalent must also be updated to remain consistent with the changed interface. Refactoring in dependency order, fewest dependents first, minimizes the coordination required at each step.
Converters Translate Syntax. Modernization Transforms Meaning.
Automated COBOL-to-Java converters are faster than human rewriting. They are also optimized for a different goal: syntactic correspondence rather than semantic equivalence in the target language’s idiom. Jobol is what syntactic correspondence produces when the source language (COBOL) and the target language (Java) have fundamentally different structural models.
The organizations that avoid Jobol are not the ones that use better converters. They are the ones that treat the converter as a draft generator rather than a finished product, invest in structural analysis before conversion starts, design the Java domain model before the converter runs, and apply systematic refactoring to the converter’s output with the business logic inventory as the validation specification.
Jobol is avoidable. The cost of avoiding it is the structural analysis and domain modeling work that should precede every conversion. The cost of not avoiding it is a Java system that requires COBOL expertise to maintain, which means the expected benefit of the modernization, the ability to maintain the system with Java developers, is never realized.
Convert the intent. Not the syntax. The difference is the entire value of the modernization.


