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:
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:
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:
- Phase 1: Migrate simple batch programs (no CICS, no SQL)
- Phase 2: Migrate programs with SQL
- Phase 3: Migrate CICS transactions
- Phase 4: Migrate complex programs
15.8.2 Side-by-Side Testing¶
Run mainframe and Java in parallel:
- Compile PL/I to Java
- Run both versions with same input
- Compare outputs byte-by-byte
- Investigate discrepancies
- 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