08 · Case study
An IBM i (AS/400) estateRPGLE · SQLRPGLE · DDS · DB2 for i · Java 21
The system
The problem
An IBM i estate is usually the system the business actually runs on and the one nobody will touch. The programs work. The people who wrote them have retired. The source is there, in fixed-format columns for the older members and free-form for the newer ones, and between the two sit twenty years of maintenance in whichever style was current that year.
What makes RPG hard to read is not the syntax. It is that the control flow is partly invisible: indicators set in one place and tested in another, the program cycle driving the older members without a single explicit loop, and record-level access — CHAIN, SETLL, READE — that reads the database a row at a time against a key path defined in a different object entirely. Read the program alone and you have half the behaviour.
What ran
A deterministic scanner in the playbook reads fixed-format RPG III and IV alongside free-form RPGLE, resolves /COPY members, and records the specifications as facts: files and their access paths, data structures with their packed and zoned fields, subprocedures and their signatures, every CALL and CALLP with its target, and the EXEC SQL statements embedded in SQLRPGLE.
An indicator that is set in one specification and tested forty lines later becomes a named boolean with the meaning the code gives it. Where an older member relies on the RPG cycle rather than writing its loop, the loop it implies is written out. This is the step that turns a member into something a Java developer can review, and it is done by the parser rather than by a model, so it is the same every time.
A CHAIN is a keyed read against a file whose key is declared in a DDS logical file, so the access path is resolved from the DDS rather than guessed from the field names. Record-level operations become repository queries against those keys. The database side of this is its own migration, and the two share one parse of the DDS.
A display file with subfiles is a paging list with function keys and field attributes driven by indicators. The questionnaire asks what it should become: an HTTP API with a new front end, a service left behind the existing 5250 screens during a phased cutover, or a batch entry point where the screen was only ever a launcher. The migration does not assume a web UI is wanted.
Each member is emitted and the project built before the next one starts. A partial run leaves a project that compiles, and a failure names the member behind it.
Packed and zoned decimal fields become BigDecimal, never double — the gate that already blocks a COBOL to Java cutover applies unchanged here. Every file the program opens is accounted for, every called program resolves to something that exists or is listed as external, and the project builds and its tests run.
What it did
A layered service instead of a member
Programs become services, subprocedures become methods, and file access moves behind repositories. The layering is the point: an RPG member mixes screen handling, business rules and database access in one object, and until those are separated nothing about the system can be tested in isolation.
The call graph across the estate
Which programs call which, which service programs they bind to, and which files each one touches — recovered from the source rather than from a spreadsheet someone maintained until 2014. This is what tells you which members can move first.
A named list of what needs a person
Machine interfaces, data areas, message queues, activation-group behaviour and any operation whose Java equivalent is not provable are listed rather than approximated. They are usually a small number of members, and they are the ones worth a conversation.
Where it ended
Whether the screens survive. Every AS/400 modernization eventually reaches the question of whether the 5250 interface is a liability or the reason the warehouse staff are fast, and that is a business answer rather than a technical one. The migration is built so the answer can be either: the service layer is the same whether a browser or a green screen sits in front of it.
What happens next
The reason to parse rather than prompt is that RPG hides its control flow in state. A model reading a member in isolation will produce plausible Java that is subtly wrong about an indicator, and plausible-but-wrong is the expensive kind. Recovering the indicators, the cycle and the access paths deterministically means the same member produces the same Java every time, and a person can check it against the original line by line.