← All cases

13 · Case study

The IBM i batch layer to jobs, queues and transactions

The batch side of an IBM i (AS/400) estateCL · SBMJOB · OVRDBF · data queues · data areas · triggers · commitment control

The system

CL program
hosted service or Spring Batch job
Submitted job
Hangfire, Quartz or Spring Scheduler
Data queue
Service Bus, RabbitMQ, Kafka or JMS
Trigger
decided per trigger

The problem

A program migration that compiles and passes its tests can still fail at month end, because the behaviour that failed was never in the program. It was in the CL that called it: the OVRDBF that rebound a file just before the call, so the file the program opens is not the file it names; the SBMJOB chain that decided what ran after what; the data area holding the last-used key; the keyed data queue another job was waiting on; the trigger that fired when a row landed; the commitment control scope that decided which files rolled back together.

CL is orchestration. It maps to job code, not to business logic, and it needs to be read as a layer of its own rather than translated line by line into whatever the programs became.

What ran

  1. Read CL as orchestration

    Every CL member for its program calls, submitted jobs, file overrides, data area and data queue use, file commands, message sends and commitment control scoping. REXX and System/36 OCL scripts drive the system by handing it CL, so they are read for the CL they issue and follow wherever the CL goes.

  2. Draw the job graph

    Job dependencies come from the CL call graph, not from a schedule someone keeps in a document. Each submitted job becomes a scheduled job on Hangfire or Quartz for .NET, Quartz or Spring Scheduler for Java, and the dependencies between them are carried across as declared.

  3. Resolve every override

    Every OVRDBF and OVRPRTF is located, with the program it affects, so the file each program actually opens is known before a line is emitted. Overrides are why a static read of the program alone gives the wrong file. They become runtime configuration or a repository parameter, and the reason each one existed is kept with it.

  4. Place the shared state

    A data area, frequently a counter or a last-used key, becomes a configuration store or a table row, and its concurrency behaviour is preserved rather than rediscovered under load. A data queue becomes a message queue or stream; a keyed data queue needs one that supports selective receive, and the mapping says so. A message file becomes a resource file or bundle so the user-facing text is carried across.

  5. Decide each trigger

    Both forms are read: the ADDPFTRG command and SQL CREATE TRIGGER, each with its table, event, timing and the program that runs. The plan decides per trigger. Keeping it in the database preserves behaviour; moving it into the application as a domain event makes it visible. Neither is the default.

  6. Match the transaction boundaries

    Commit and rollback points and commitment control scoping become a transaction scope on .NET or a declarative transaction on Java. The boundary has to match the original exactly, including which files are under commit, and it is checked against the source rather than assumed from the code that moved.

  7. Keep the reports readable

    A printer file becomes a reporting component or a generated PDF. The column positions in the printer file define the layout, so the report the operations team reads on Monday looks like the one it read on Friday.

What it produced

Job definitions with their dependencies

One scheduled job per submitted job, ordered by the CL call graph, in the scheduler the target stack already uses.

Configuration for what overrides used to do

Each rebinding named, with the program it applied to and the file it pointed at, as configuration a reviewer can read instead of a command that ran silently before the call.

A queue map, a trigger list and a transaction list

Which data queues become which queues or topics, and which of them are keyed. Every trigger with its decision. Every commit scope with the files under it. These are the three lists a cutover rehearsal runs against.

Where it ended

Whether each trigger stays in the database. It is a choice between behaviour that is preserved and behaviour that is visible, and it is made one trigger at a time with the table, the event and the program in front of you.

What happens next

  • Run the database migration first and the programs alongside. The batch layer calls both, and all three come from the same read of the export.
  • Rehearse a period-end with the three lists. The override, the queue key and the commit scope are what a compile cannot check.
  • Where a JT400 or XMLSERVICE bridge fed a job from outside, it becomes a direct in-process call once the job has moved, and is removed rather than reimplemented.

The reason to treat batch as its own migration is that its failures arrive late. A program that is wrong fails in a test on Tuesday; a job graph that is wrong fails at month end, in production, after the old system is off. Reading the CL as a layer, with every override and commit scope located, is what moves that discovery to the plan.