Skip to content

Latest commit

 

History

History
319 lines (266 loc) · 12.9 KB

File metadata and controls

319 lines (266 loc) · 12.9 KB
  1. High-Level Architecture The migrated system adopts a layered, Spring Boot-based architecture that preserves the batch semantics of the original COBOL program while introducing modern patterns: dependency injection, declarative transaction management, and structured observability.

Architecture Layers

[ Flat File Input ] ──> [ FlatFileItemReader ] ──> [ AccountItemProcessor ] | [ InterestCalculationService ] [ BalanceValidationService ] | [ Flat File Output ] <── [ FlatFileItemWriter ] <── [ AccountOutputRecord ] | [ RunControlCounters / Metrics ]

Technology stack: • Java 17 (LTS) • Spring Batch 5 for chunk-oriented processing • Spring Boot 3 for auto-configuration and embedded job launcher • Micrometer + Prometheus for operational metrics (replaces DISPLAY counters) • Testcontainers for integration tests

  1. Core Components 3.1 AccountRecord (maps COBOL ACCT-IN-REC) // src/main/java/com/example/acctproc/model/AccountRecord.java public record AccountRecord( long customerId, // AI-CUST-ID PIC 9(10) String lastName, // AI-LAST-NAME PIC X(20) String firstName, // AI-FIRST-NAME PIC X(15) BigDecimal balance, // AI-BALANCE PIC S9(11)V99 COMP-3 AccountType accountType, // AI-ACCT-TYPE 88 TYPE-SAVINGS / TYPE-CHECKING AccountStatus status // AI-STATUS 88 STATUS-ACTIVE / STATUS-CLOSED ) {}

public enum AccountType { SAVINGS, CHECKING } public enum AccountStatus { ACTIVE, CLOSED }

3.2 AccountOutputRecord (maps COBOL ACCT-OUT-REC) public record AccountOutputRecord( long customerId, // AO-CUST-ID BigDecimal newBalance, // AO-NEW-BALANCE BigDecimal interestAmt, // AO-INTEREST ReturnCode returnCode // AO-RETURN-CODE ) {}

public enum ReturnCode { OK, LB, SK } // OK=ok, LB=low-balance, SK=skipped

3.3 RunControlCounters (maps COBOL WS-COUNTERS) @Component public class RunControlCounters { private final AtomicLong totalRead = new AtomicLong(); // WS-TOTAL-READ private final AtomicLong totalProcessed = new AtomicLong(); // WS-TOTAL-PROC private final AtomicLong totalSkipped = new AtomicLong(); // WS-TOTAL-ERR

public void incrementRead()      { totalRead.incrementAndGet(); }
public void incrementProcessed() { totalProcessed.incrementAndGet(); }
public void incrementSkipped()   { totalSkipped.incrementAndGet(); }
public void printSummary() {
    log.info("RECORDS READ:      {}", totalRead.get());
    log.info("RECORDS PROCESSED: {}", totalProcessed.get());
    log.info("RECORDS SKIPPED:   {}", totalSkipped.get());
}

}

  1. Interfaces 4.1 InterestCalculationService Encapsulates COBOL paragraph 2100-CALC-INTEREST. public interface InterestCalculationService { /**
    • COBOL: COMPUTE WS-INTEREST-AMT = AI-BALANCE * WS-INTEREST-RATE
    • Rate applied only to SAVINGS accounts; CHECKING returns ZERO. */ BigDecimal calculate(AccountRecord account); }

4.2 BalanceValidationService Encapsulates COBOL paragraph 2200-VALIDATE-BALANCE. public interface BalanceValidationService { /** * COBOL: IF AI-BALANCE < WS-MIN-BALANCE -> 'LB' ELSE -> 'OK' */ ReturnCode validate(AccountRecord account); }

4.3 AccountItemProcessor (Spring Batch ItemProcessor) Replaces COBOL paragraph 2000-PROCESS-ACCOUNTS. public interface AccountItemProcessor extends ItemProcessor<AccountRecord, AccountOutputRecord> { @Override AccountOutputRecord process(AccountRecord item) throws Exception; }

  1. API Signatures 5.1 InterestCalculationServiceImpl @Service public class InterestCalculationServiceImpl implements InterestCalculationService {

    private static final BigDecimal SAVINGS_RATE = new BigDecimal("0.0325"); // WS-INTEREST-RATE PIC V9(4) VALUE .0325

    @Override public BigDecimal calculate(AccountRecord account) { if (account.accountType() == AccountType.SAVINGS) { return account.balance() .multiply(SAVINGS_RATE) .setScale(2, RoundingMode.HALF_EVEN); } return BigDecimal.ZERO; } }

5.2 BalanceValidationServiceImpl @Service public class BalanceValidationServiceImpl implements BalanceValidationService {

private static final BigDecimal MIN_BALANCE =
    new BigDecimal("100.00");  // WS-MIN-BALANCE PIC 9(9)V99 VALUE 100.00

@Override
public ReturnCode validate(AccountRecord account) {
    return account.balance().compareTo(MIN_BALANCE) < 0
        ? ReturnCode.LB   // 'LB' low-balance
        : ReturnCode.OK;  // 'OK' passed
}

}

5.3 AccountItemProcessorImpl @Component public class AccountItemProcessorImpl implements AccountItemProcessor {

private final InterestCalculationService interestSvc;
private final BalanceValidationService   validationSvc;
private final RunControlCounters         counters;

@Override
public AccountOutputRecord process(AccountRecord item) {
    counters.incrementRead();
    if (item.status() != AccountStatus.ACTIVE) {
        counters.incrementSkipped();
        return new AccountOutputRecord(
            item.customerId(), item.balance(),
            BigDecimal.ZERO, ReturnCode.SK);
    }
    BigDecimal interest   = interestSvc.calculate(item);
    ReturnCode returnCode = validationSvc.validate(item);
    BigDecimal newBalance = item.balance().add(interest);
    counters.incrementProcessed();
    return new AccountOutputRecord(
        item.customerId(), newBalance, interest, returnCode);
}

}

5.4 Batch Job Configuration @Configuration public class AccountProcessingJobConfig {

@Bean
public FlatFileItemReader<AccountRecord> accountReader(
        @Value("${job.input.file}") Resource resource) { ... }

@Bean
public FlatFileItemWriter<AccountOutputRecord> accountWriter(
        @Value("${job.output.file}") Resource resource) { ... }

@Bean
public Step processAccountsStep(...) {
    return stepBuilderFactory
        .get("processAccountsStep")
        .<AccountRecord, AccountOutputRecord>chunk(500)
        .reader(accountReader(null))
        .processor(processor)
        .writer(accountWriter(null))
        .faultTolerant()
        .skip(FlatFileParseException.class).skipLimit(100)
        .build();
}

@Bean
public Job accountProcessingJob(Step processAccountsStep) {
    return jobBuilderFactory
        .get("accountProcessingJob")
        .start(processAccountsStep)
        .listener(runControlListener)
        .build();
}

}

  1. Data Models The table below maps every significant COBOL data item to its Java equivalent, preserving semantic precision (especially fixed-decimal arithmetic).

COBOL Item COBOL Picture Java Type Notes AI-CUST-ID PIC 9(10) long 10-digit unsigned integer AI-LAST-NAME PIC X(20) String Trimmed on read AI-FIRST-NAME PIC X(15) String Trimmed on read AI-BALANCE PIC S9(11)V99 COMP-3 BigDecimal(2) HALF_EVEN rounding; COMP-3 packed decimal AI-ACCT-TYPE 88 SA/CH AccountType enum SAVINGS | CHECKING AI-STATUS 88 A/C AccountStatus enum ACTIVE | CLOSED WS-INTEREST-RATE PIC V9(4) VALUE .0325 BigDecimal("0.0325") Constant; injected via @Value WS-MIN-BALANCE PIC 9(9)V99 VALUE 100.00 BigDecimal("100.00") Configurable via application.yml WS-TOTAL-READ PIC 9(7) AtomicLong Thread-safe counter WS-TOTAL-PROC PIC 9(7) AtomicLong Thread-safe counter WS-TOTAL-ERR PIC 9(7) AtomicLong Thread-safe counter AO-RETURN-CODE PIC XX OK/LB/SK ReturnCode enum OK | LB (low-balance) | SK (skipped)

  1. Testing Strategy The testing pyramid mirrors the COBOL paragraph structure: each paragraph becomes a unit, each division becomes an integration surface.

Layer Framework Scope COBOL Equivalent Unit JUnit 5 + Mockito Service classes in isolation Paragraph-level logic Slice Spring Batch Test Single Step with mock I/O Division-level flow Integration Testcontainers + embedded job Full job on real files Full PERFORM … UNTIL flow Contract Pact Input/output file schemas FD record layouts

7.1 Unit Test Example — InterestCalculationService @Test void savingsAccount_appliesRate() { var acct = new AccountRecord(1L, "Smith", "John", new BigDecimal("10000.00"), AccountType.SAVINGS, AccountStatus.ACTIVE); BigDecimal interest = svc.calculate(acct); // COBOL: COMPUTE WS-INTEREST-AMT = 10000.00 * .0325 = 325.00 assertThat(interest).isEqualByComparingTo("325.00"); }

@Test void checkingAccount_returnsZero() { var acct = new AccountRecord(2L, "Doe", "Jane", new BigDecimal("5000.00"), AccountType.CHECKING, AccountStatus.ACTIVE); assertThat(svc.calculate(acct)).isEqualByComparingTo(BigDecimal.ZERO); }

7.2 Unit Test Example — BalanceValidationService @ParameterizedTest @CsvSource({ "99.99,LB", "100.00,OK", "100.01,OK" }) void balanceThreshold(String balanceStr, ReturnCode expected) { var acct = buildAccount(new BigDecimal(balanceStr)); assertThat(svc.validate(acct)).isEqualTo(expected); }

  1. Error Recovery Strategy The COBOL program has no explicit error recovery — a bad record causes ABEND. The Java migration replaces this with Spring Batch's built-in fault-tolerance model.

Failure Type COBOL Behaviour Java Recovery Parse error ABEND / job terminates Skip record up to skipLimit=100; write to dead-letter file Numeric overflow ABEND or silent truncation ArithmeticException caught; record skipped, counter incremented I/O error on input ABEND Non-skippable; job fails fast, restart from checkpoint I/O error on output ABEND Non-skippable; job fails fast, restart from last committed chunk Closed account WS-TOTAL-ERR += 1 ReturnCode.SK; record written to output with SK code

8.1 Skip Listener @Component public class AccountSkipListener implements SkipListener<AccountRecord, AccountOutputRecord> {

@Override
public void onSkipInProcess(AccountRecord item, Throwable t) {
    log.error("SKIP cust={} reason={}", item.customerId(), t.getMessage());
    counters.incrementSkipped();
    deadLetterWriter.write(item);  // audit trail for ops team
}

}

8.2 Restart & Checkpoint Spring Batch persists job execution state to a JobRepository (H2 for dev, PostgreSQL for prod). If the job fails mid-run, re-executing the same JobInstance resumes from the last committed chunk boundary — equivalent to a JCL RESTART on the mainframe.

  1. Integration Test Strategy 9.1 Full-Job Integration Test Uses a curated golden input file that exercises every code path from the COBOL PROCEDURE DIVISION and asserts on the output file contents and run-control counters. @SpringBatchTest @SpringBootTest class AccountProcessingJobIT {

    @Autowired JobLauncherTestUtils jobLauncherTestUtils; @Autowired RunControlCounters counters;

    @Test void fullJob_matchesCobolGoldenOutput() throws Exception { // Input: 5 ACTIVE SAVINGS, 2 ACTIVE CHECKING, 1 CLOSED JobExecution exec = jobLauncherTestUtils.launchJob();

     assertThat(exec.getStatus()).isEqualTo(BatchStatus.COMPLETED);
     assertThat(counters.totalRead()).isEqualTo(8L);
     assertThat(counters.totalProcessed()).isEqualTo(7L);
     assertThat(counters.totalSkipped()).isEqualTo(1L);
    
     // Assert output matches COBOL ACCT-OUT.DAT golden file byte-for-byte
     assertThat(outputFile).hasSameTextualContentAs(goldenOutputFile);
    

    } }

9.2 Parallel Execution Smoke Test Validates that multiple job instances (different input partitions) do not corrupt shared counters. Spring Batch's JobScope ensures each job execution gets isolated beans. @Test void concurrentJobs_isolatedCounters() throws Exception { var f1 = executor.submit(() -> launchJob("partition-1.dat")); var f2 = executor.submit(() -> launchJob("partition-2.dat")); assertThat(f1.get().getStatus()).isEqualTo(BatchStatus.COMPLETED); assertThat(f2.get().getStatus()).isEqualTo(BatchStatus.COMPLETED); // Each job has its own RunControlCounters bean — no shared state }

9.3 Restart Recovery Test @Test void failedJob_restartsFromCheckpoint() throws Exception { // First run: inject I/O failure at record 500 JobExecution failed = launchJobWithFault(500); assertThat(failed.getStatus()).isEqualTo(BatchStatus.FAILED);

// Second run: same JobInstance, resumes from committed chunk
JobExecution restarted = jobLauncherTestUtils.launchJob();
assertThat(restarted.getStatus()).isEqualTo(BatchStatus.COMPLETED);
// Output file contains ALL records (first 499 from run 1 + rest from run 2)
assertOutputRecordCount(TOTAL_INPUT_RECORDS);

}