Skip to content

Part 15 — Best Practices

15.1 Overview

This section provides recommended best practices for developing, deploying, and maintaining PL/I applications on the Heirloom PLI Platform. Following these guidelines will help ensure code quality, performance, and maintainability.


15.2 Code Organization

15.2.1 Project Structure

Recommended directory structure:

my-pli-application/
├── src/
│   ├── main/
│   │   ├── pli/                    ← PL/I source programs
│   │   │   ├── programs/           ← Main programs
│   │   │   │   ├── H1*.pli        ← High-level transaction handlers
│   │   │   │   ├── H8*.pli        ← Service/utility modules
│   │   │   │   └── H9*.pli        ← Data access modules
│   │   │   ├── includes/           ← Copybooks and includes
│   │   │   │   ├── common/        ← Common definitions
│   │   │   │   ├── structures/    ← Data structures
│   │   │   │   └── sql/           ← SQL declarations
│   │   │   └── macros/            ← Macro definitions
│   │   ├── jcl/                   ← JCL job definitions (for batch)
│   │   ├── java/                  ← Hand-written Java code (if any)
│   │   └── resources/
│   │       ├── helper.json        ← Helper configuration
│   │       ├── cics-config.xml    ← CICS configuration
│   │       └── application.properties
│   └── test/
│       ├── java/                  ← JUnit tests
│       └── resources/             ← Test data
├── generated/                      ← Generated Java (created by compiler)
├── lib/                           ← Compiler JARs
│   └── pli_compiler2-*.jar
├── config/                        ← Configuration files
│   ├── pli.properties
│   └── logback.xml
├── docs/                          ← Documentation
├── build.gradle                   ← Build configuration
└── README.md

15.2.2 Program Organization Patterns

Pattern: Layered Architecture

Organize programs by responsibility:

H1 Programs — High-level transaction handlers - Entry points for CICS transactions - Coordinate workflow - Handle user interface logic - Call H8 service modules

H8 Programs — Service/utility modules - Business logic implementation - Validation and calculations - Orchestrate H9 data access - Reusable across multiple H1 programs

H9 Programs — Data access modules - Database I/O operations - SQL queries and updates - File operations - Single responsibility per module

Example:

H1CUST01.pli → Customer Inquiry (transaction handler)
    ↓ calls
H8CUSTV1.pli → Customer Validation (service)
    ↓ calls
H9CUSTR1.pli → Customer Read (data access)

H1CUST02.pli → Customer Update (transaction handler)
    ↓ calls
H8CUSTV1.pli → Customer Validation (same service)
    ↓ calls
H9CUSTU1.pli → Customer Update (data access)


15.2.3 Naming Conventions

Program names: - H1xxxxx — Transaction handlers (6 characters) - H8xxxxx — Service modules (6 characters) - H9xxxxx — Data access modules (6 characters) - Use consistent prefixes (e.g., CUST for customer, ORD for order)

Variable names:

/* Use descriptive names */
DCL CUSTOMER_ID CHAR(10);        /* Good */
DCL CID CHAR(10);                /* Avoid abbreviations */

/* Use prefixes for scope */
DCL WS_COUNTER FIXED BIN(31);    /* Working storage */
DCL IN_CUSTOMER_ID CHAR(10);     /* Input parameter */
DCL OUT_STATUS_CODE CHAR(2);     /* Output parameter */

/* Constants in uppercase */
DCL MAX_RECORDS FIXED BIN(31) INIT(1000);
DCL STATUS_ACTIVE CHAR(1) INIT('A');

Structure names:

/* Use _REC suffix for records */
DCL 1 CUSTOMER_REC,
      2 CUST_ID      CHAR(10),
      2 CUST_NAME    CHAR(30);

/* Use _AREA suffix for communication areas */
DCL 1 COMMAREA,
      2 CA_FUNCTION  CHAR(4),
      2 CA_DATA      CHAR(1000);


15.3 Coding Best Practices

15.3.1 Data Declaration

DO: ✅ Declare all variables explicitly ✅ Use appropriate data types ✅ Initialize variables ✅ Group related fields in structures ✅ Use copybooks for shared definitions

/* Good practice */
DCL CUSTOMER_ID CHAR(10) INIT('');
DCL RECORD_COUNT FIXED BIN(31) INIT(0);

/* Use copybooks */
%INCLUDE CUSTDEF;  /* Instead of duplicating declarations */

DON'T: ❌ Rely on implicit declarations ❌ Use overly large arrays ❌ Duplicate structure definitions

/* Avoid */
DCL HUGE_ARRAY(1000000) CHAR(1000);  /* 1GB memory! */

/* Instead, use dynamic allocation or database */

15.3.2 Error Handling

Always handle errors:

/* SQL error handling */
EXEC SQL
  SELECT NAME INTO :CUSTOMER_NAME
  FROM CUSTOMERS
  WHERE CUSTOMER_ID = :CUSTOMER_ID;

IF SQLCODE = 0 THEN
  /* Success */
  PUT SKIP LIST('Customer found:', CUSTOMER_NAME);
ELSE IF SQLCODE = 100 THEN
  /* Not found */
  PUT SKIP LIST('Customer not found');
ELSE
  /* Error */
  PUT SKIP LIST('SQL Error:', SQLCODE);
END;

/* CICS error handling */
EXEC CICS READ
  DATASET('CUSTFILE')
  INTO(CUSTOMER_REC)
  RIDFLD(CUSTOMER_ID)
  RESP(WS_RESP)
  RESP2(WS_RESP2);

IF WS_RESP = DFHRESP(NORMAL) THEN
  /* Success */
ELSE IF WS_RESP = DFHRESP(NOTFND) THEN
  /* Record not found */
ELSE
  /* Other error */
  PERFORM ERROR_HANDLER;
END;

15.3.3 Resource Management

Close resources properly:

/* File handling */
OPEN FILE(INPUT_FILE);

/* Process file */
ON ENDFILE(INPUT_FILE) GO TO CLEANUP;
DO WHILE('1'B);
  READ FILE(INPUT_FILE) INTO(INPUT_REC);
  /* Process record */
END;

CLEANUP:
  CLOSE FILE(INPUT_FILE);  /* Always close */

/* SQL cursors */
EXEC SQL DECLARE CUST_CURSOR CURSOR FOR
  SELECT * FROM CUSTOMERS;

EXEC SQL OPEN CUST_CURSOR;

/* Fetch rows */

EXEC SQL CLOSE CUST_CURSOR;  /* Always close */

15.3.4 Performance Optimization

Efficient SQL:

/* Good: Single query with JOIN */
EXEC SQL
  SELECT C.NAME, O.ORDER_DATE, O.AMOUNT
  INTO :CUSTOMER_NAME, :ORDER_DATE, :ORDER_AMOUNT
  FROM CUSTOMERS C
  JOIN ORDERS O ON C.CUSTOMER_ID = O.CUSTOMER_ID
  WHERE C.CUSTOMER_ID = :CUSTOMER_ID;

/* Avoid: Multiple queries in loop */
/* This is slow! */
DO I = 1 TO 1000;
  EXEC SQL
    SELECT NAME INTO :NAME
    FROM CUSTOMERS
    WHERE CUSTOMER_ID = :CUST_IDS(I);
  /* Process */
END;

/* Better: Batch with WHERE IN */
EXEC SQL
  SELECT CUSTOMER_ID, NAME
  INTO :CUST_ID, :NAME
  FROM CUSTOMERS
  WHERE CUSTOMER_ID IN (:CUST_ID1, :CUST_ID2, ..., :CUST_ID1000);

Efficient arrays:

/* Use appropriate array sizes */
DCL ITEMS(1000) CHAR(20);  /* Reasonable size */

/* Not: DCL ITEMS(1000000) CHAR(1000); ← 1GB! */

15.4 Build and Deployment

15.4.1 Build Configuration

Version everything:

// build.gradle
group = 'com.yourcompany'
version = '1.2.3'  // Semantic versioning

dependencies {
    // Pin versions
    implementation 'com.heirloom:pli_runtime2:26.2.27.RC1'  // Specific version
    // Not: implementation 'com.heirloom:pli_runtime2:+'  ← Avoid wildcards
}

15.4.2 Configuration Management

Externalize configuration:

Don't hardcode:

/* Avoid hardcoded values */
DCL DB_HOST CHAR(20) INIT('prod-db-01.company.com');  /* Bad */

Use configuration files:

# config/pli.properties
jdbc.url=jdbc:db2://prod-db-01.company.com:50000/CUSTDB
jdbc.username=appuser
jdbc.password=${DB_PASSWORD}  # From environment

cics.region=PROD1
cics.applid=CUSTAPP

Different configs for environments:

config/
├── dev.properties
├── test.properties
└── prod.properties


15.4.3 Version Control

Git best practices:

.gitignore:

# Build artifacts
build/
generated/
*.class
*.jar
*.war

# IDE files
.idea/
.vscode/
*.iml

# Logs
*.log

# Configuration (if contains secrets)
config/prod.properties

# But include:
# - All PL/I source (.pli)
# - Build configuration (build.gradle, pom.xml)
# - Helper configuration (helper.json)

Commit messages:

feat: Add customer inquiry transaction (H1CUST01)
fix: Correct balance calculation in H8BALA01
docs: Update README with deployment instructions
refactor: Extract validation to H8CUSTV1


15.5 Testing

15.5.1 Unit Testing

Test generated Java code:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class H9CUSTR1Test {
    @Test
    void testCustomerRead() {
        H9CUSTR1 program = new H9CUSTR1();

        // Setup test data
        program.setCustomerId("CUST001");

        // Execute
        program.execute();

        // Verify
        assertEquals("John Smith", program.getCustomerName());
        assertEquals(1234.56, program.getBalance().doubleValue(), 0.01);
    }
}

15.5.2 Integration Testing

Test database integration:

@Test
void testSQLCustomerQuery() throws Exception {
    // Setup test database
    TestDatabase.insertCustomer("CUST001", "Test Customer", 100.00);

    // Execute program
    H9CUSTR1 program = new H9CUSTR1();
    program.setCustomerId("CUST001");
    program.execute();

    // Verify
    assertEquals("Test Customer", program.getCustomerName());

    // Cleanup
    TestDatabase.cleanup();
}

15.5.3 CICS Testing

Use automated testing:

For CICS applications, use tools like: - Playwright for web UI testing - REST API testing for transaction calls - Mock CICS environment for unit tests


15.6 Security

15.6.1 SQL Injection Prevention

Always use parameterized queries:

/* SAFE: Parameterized query */
EXEC SQL
  SELECT NAME INTO :CUSTOMER_NAME
  FROM CUSTOMERS
  WHERE CUSTOMER_ID = :CUSTOMER_ID;  /* ← Parameter, not concatenated */

/* NEVER build SQL with string concatenation */
/* This is vulnerable to SQL injection! */

15.6.2 Sensitive Data

Don't log sensitive data:

/* Bad */
PUT SKIP LIST('Processing card:', CREDIT_CARD_NUMBER);  /* Don't log PII */

/* Good */
PUT SKIP LIST('Processing card:', MASK_CARD(CREDIT_CARD_NUMBER));
/* Or just: */
PUT SKIP LIST('Processing card ending in:', SUBSTR(CREDIT_CARD_NUMBER, 13, 4));

Encrypt sensitive data in storage: - Use database encryption - Encrypt fields in Group structures if needed - Never commit secrets to version control


15.6.3 Access Control

Validate authorization:

/* Check user authorization before operations */
EXEC CICS VERIFY
  PASSWORD(USER_PASSWORD)
  USERID(USER_ID)
  RESP(WS_RESP);

IF WS_RESP ^= DFHRESP(NORMAL) THEN
  /* Unauthorized */
  PERFORM UNAUTHORIZED_ACCESS;
  RETURN;
END;

15.7 Monitoring and Observability

15.7.1 Logging

Log appropriately:

/* Log important events */
PUT SKIP LIST('INFO: Processing customer:', CUSTOMER_ID);
PUT SKIP LIST('INFO: Records processed:', RECORD_COUNT);

/* Log errors with context */
PUT SKIP LIST('ERROR: SQL failed. SQLCODE:', SQLCODE);
PUT SKIP LIST('ERROR: Customer ID:', CUSTOMER_ID);

/* Use structured logging in Java layer */

Log levels: - TRACE: Very detailed debugging - DEBUG: Debugging information - INFO: Informational messages - WARN: Warning conditions - ERROR: Error conditions - FATAL: Critical errors


15.7.2 Performance Monitoring

Add performance metrics:

/* Measure execution time */
DCL START_TIME FIXED BIN(31);
DCL END_TIME FIXED BIN(31);
DCL ELAPSED_TIME FIXED BIN(31);

START_TIME = TIME();

/* Execute business logic */
PERFORM PROCESS_RECORDS;

END_TIME = TIME();
ELAPSED_TIME = END_TIME - START_TIME;

PUT SKIP LIST('Execution time (ms):', ELAPSED_TIME);

15.8 Migration Best Practices

15.8.1 Incremental Migration

Don't migrate everything at once:

  1. Phase 1: Migrate simple batch programs (no CICS, no SQL)
  2. Phase 2: Migrate programs with SQL
  3. Phase 3: Migrate CICS transactions
  4. Phase 4: Migrate complex programs

15.8.2 Side-by-Side Testing

Run mainframe and Java in parallel:

  1. Compile PL/I to Java
  2. Run both versions with same input
  3. Compare outputs byte-by-byte
  4. Investigate discrepancies
  5. Iterate until outputs match

15.8.3 Data Validation

Verify data integrity:

/* Compare record counts */
SELECT COUNT(*) FROM MAINFRAME_TABLE;  /* Mainframe */
SELECT COUNT(*) FROM JAVA_TABLE;       /* Java */

/* Compare checksums */
SELECT SUM(BALANCE) FROM CUSTOMERS WHERE STATUS = 'A';

15.9 Documentation

15.9.1 Code Documentation

Document complex logic:

/* Document non-obvious logic */

/* Calculate compound interest using:
 * A = P(1 + r/n)^(nt)
 * Where:
 *   P = principal amount
 *   r = annual interest rate
 *   n = compounding frequency
 *   t = time in years
 */
INTEREST = PRINCIPAL * ((1 + RATE / FREQUENCY) ** (FREQUENCY * YEARS));

15.9.2 Program Documentation

Maintain README files:

# H1CUST01 - Customer Inquiry Transaction

## Purpose
Displays customer information including name, address, and account balance.

## Transaction ID
CUST

## Inputs
- Customer ID (10 characters)

## Outputs
- Customer name
- Address
- Account balance

## Dependencies
- H8CUSTV1 (Customer validation)
- H9CUSTR1 (Customer read)

## Database Tables
- CUSTOMERS (read)

## Error Handling
- NOTFND: Customer not found
- SQLERR: Database error

15.10 Maintenance

15.10.1 Regular Updates

Keep dependencies current:

# Check for updates
./gradlew dependencyUpdates

# Update PLI compiler
# Review release notes first!
# Update version in build.gradle
implementation 'com.heirloom:pli_runtime2:26.3.0.RC1'

15.10.2 Code Reviews

Review checklist:

  • Code follows naming conventions
  • Error handling implemented
  • Resources properly closed
  • SQL queries parameterized
  • Sensitive data not logged
  • Tests included
  • Documentation updated
  • No hardcoded configuration

15.10.3 Refactoring

When to refactor: - Code duplication (extract to H8 service module) - Overly complex procedures (break into smaller procedures) - Long parameter lists (use structures) - Magic numbers (extract to named constants)

How to refactor safely: 1. Write tests first 2. Make small changes 3. Run tests after each change 4. Commit frequently


15.11 Summary Checklist

Development

  • Follow project structure conventions
  • Use layered architecture (H1/H8/H9)
  • Apply naming conventions consistently
  • Handle all errors
  • Close all resources
  • Use parameterized SQL queries
  • Externalize configuration
  • Write unit tests

Build

  • Pin dependency versions
  • Enable Manifold plugin
  • Configure CI/CD pipeline
  • Use environment-specific configs

Deployment

  • Test in non-production first
  • Verify all dependencies deployed
  • Check configuration files
  • Monitor logs after deployment
  • Have rollback plan ready

Security

  • No SQL injection vulnerabilities
  • No sensitive data in logs
  • No secrets in version control
  • Validate authorization
  • Encrypt sensitive data

Monitoring

  • Logging configured
  • Performance metrics tracked
  • Error alerting set up
  • Regular log review