“This week I’ve been mostly writing sub-procedures”
If you have spent any time writing modern RPGLE on IBM i, you’ve seen small sub-procedures, bloated sub-procedures, simple, complex and plain crazy sub-procedures.
Over the last week, I’ve been working hard to modernize a whole bunch of legacy RPG and SQL RPG code into cohesive, reusable sub-procedures and ‘Wow it’s been fun!’ playing with various other programmer’s old code. Legacy programmers who had a whole range of design ideas about how complex subprocedures, and subroutines, should be. Sprinkle in a complete lack of common naming standards, indicators rather than variables. some wonderful spaghetti code, and you will get the idea why I started thinking about:
What is the best modern way to design sub-procedures?
The #1 thing to think about when designing an RPGLE sub‑procedure is:
Define the procedure’s single, clear responsibility before you write a single line of code.
Everything else – parameters, return values, error handling, performance, and modularity flows from this one decision.
If you can’t describe the procedure’s purpose in one short sentence, it’s already doing too much.
What is the one thing this procedure is responsible for?
A strong sub‑procedure:
- performs one conceptual task
- has one reason to change
- has one clear outcome
- has a predictable interface
- avoids mixing concerns (I/O, business rules, formatting, DB access)
This is the heart of cohesion, and cohesion is the foundation of every good RPGLE procedure.
Having said that, chasing tiny procedures is not the same as writing good ones.
One of the easiest traps in procedure-driven RPG is thinking that “small” automatically equals “well-designed”. Spoiler: it does not. Size and cohesion are related, but they are definitely not the same thing.
A truly cohesive procedure has one clear responsibility. It exists to answer one business question or perform one business action. When it starts trying to do two or three things at once, it stops being a helpful building block and turns into a maintenance headache that will haunt the next developer (probably you in six months time).
If you cannot name it cleanly, it is doing too much
Naming is one of the best cohesion tests we have in RPG. When you struggle to come up with a short, precise name for a procedure, that is usually your code trying to tell you something.
Bad (vague) names hide multiple responsibilities:
- ProcessOrder
- HandleInventoryUpdate
- DoTheThingWithTheCustomer
Good (clear) names force you to keep things focused:
- ValidateCustomerCredit
- CalculateOrderTotal
- CheckItemAvailability
If you find yourself writing a name like ProcessOrderAndUpdateStockAndSendEmail you are likely trying to do too many things at once! This is bad design because parameters become messy and bloated as you keep adding flags and options just to control all the different behaviors. Return values turn inconsistent because the same procedure sometimes returns a status code, sometimes a calculated result, and sometimes nothing at all depending on which hidden path it took. Testing becomes painful since you need a dozen different setups just to cover every possible combination of responsibilities. Reuse becomes almost impossible because calling the procedure from a new context risks triggering unwanted side effects you did not even know existed. Bugs creep in from those hidden side effects, and before you know it the once-simple procedure has grown into a mini-program all by itself instead of staying a clean, focused building block. That is exactly why cohesion matters so much in modern RPGLE.
But when you get it right, the procedure becomes:
- easy to test
- easy to reuse
- easy to explain
- easy to modernize
- easy to wrap in a service program
- easy to expose as an API
It’s the difference between “a chunk of code” and “a clean, modern RPG procedure”.
Business rules versus technical plumbing
Another classic cohesion killer is mixing business logic with technical details. A procedure that decides whether a customer order can be shipped should not also be opening files, handling commitment control, writing audit records, and formatting the email.
Keep them separate:
- One procedure for the business decision (IsOrderShippable)
- Another for the technical work (PersistShipmentRecord or SendOrderConfirmation)
Business procedures answer “what” and “why”. Technical procedures handle “how”. When you keep those responsibilities apart, your code becomes far easier to test, reuse, and explain to the new kid on the team.
Keep your procedures healthy
A healthy service program should represent one clear function or capability.
If you cannot describe what the entire service program does in one sensible sentence, it has probably grown too big and lost its way. Cohesive procedures are predictable. They do exactly what their name and interface promise and nothing more. That predictability is what lets your RPG applications scale cleanly over years instead of turning into a bowl of spaghetti code.
Code Sample – Legacy Subroutine vs Modernized Sub-Procedures
Here is a demonstration code sample of a typical old style RPG400 piece of code with a subroutine handling these three main functions:
- Validate Credit
- Calculate Order Total
- Check Item Availability
FQPRINT O F 132 PRINTER
I* Order Line Structure (Demo Only)
I DS
I 1 10 OITEM
I 11 13 OQTY 0
I 14 19 OPRICE 2
C* Mainline
C EXSR PROCESSORD
C SETON LR
C*-------------------------------------------------------------
C* Subroutine: PROCESSORD
C* This subroutine does EVERYTHING:
C* - Calculates order total
C* - Validates customer credit
C* - Checks item availability
C*-------------------------------------------------------------
C PROCESSORD BEGSR
C* Demo order lines (normally read from file)
C Z-ADD 0 TOTAL
C MOVE 'WIDGET001' OITEM
C Z-ADD 5 OQTY
C Z-ADD 1995 OPRICE
C MVR OQTY QTY
C MVR OPRICE PRICE
C MULT PRICE LINEAMT
C ADD LINEAMT TOTAL
C MOVE 'GADGET002' OITEM
C Z-ADD 2 OQTY
C Z-ADD 4950 OPRICE
C MVR OQTY QTY
C MVR OPRICE PRICE
C MULT PRICE LINEAMT
C ADD LINEAMT TOTAL
C*-------------------------------------------------------------
C* Validate Customer Credit (Hardcoded Limit)
C*-------------------------------------------------------------
C Z-ADD 25000 CREDITLIM
C COMP TOTAL CREDITLIM
C IFGT
C MOVE 'N' OKFLAG
C ELSE
C MOVE 'Y' OKFLAG
C ENDIF
C IF OKFLAG = 'N'
C EXCPT NOCREDIT
C ENDIF
C*-------------------------------------------------------------
C* Check Item Availability (Hardcoded Inventory)
C*-------------------------------------------------------------
C MOVE 'WIDGET001' ITEM
C Z-ADD 10 ONHAND
C Z-ADD 5 NEED
C COMP NEED ONHAND
C IFGT
C EXCPT NOITEM
C ENDIF
C MOVE 'GADGET002' ITEM
C Z-ADD 1 ONHAND
C Z-ADD 2 NEED
C COMP NEED ONHAND
C IFGT
C EXCPT NOITEM
C ENDIF
C*-------------------------------------------------------------
C* Print Total
C*-------------------------------------------------------------
C EXCPT TOTALREC
C ENDSR
OQPRINT E NOCREDIT
O 'CUSTOMER CREDIT LIMIT EXCEEDED'
OQPRINT E NOITEM
O 'ITEM NOT AVAILABLE IN INVENTORY'
OQPRINT E TOTALREC
O 'ORDER TOTAL: ' 10
O TOTAL 12 2
Now what might this look like after some code modernization into a modern RPGLE program?
Let’s modernise this from the single subroutine into three separate and cohesive sub-procedures:
**FREE
// ============================================================================
// Program: Order Processing - Modernized with Subprocedures
// Description: Demonstrates proper separation of concerns using subprocedures
// Refactored from monolithic PROCESSORD subroutine into three
// focused subprocedures:
// - CalculateOrderTotal
// - ValidateCustomerCredit
// - CheckItemAvailability
// ============================================================================
Ctl-Opt DftActGrp(*No) ActGrp(*New) Option(*SrcStmt:*NoDebugIO);
// File declarations
Dcl-F QPRINT Printer(132) Usage(*Output);
// Data structures
Dcl-Ds OrderLine Qualified;
Item Char(10);
Qty Packed(3:0);
Price Packed(6:2);
End-Ds;
// Global variables
Dcl-S OrderTotal Packed(9:2);
Dcl-S CreditApproved Ind;
// ============================================================================
// Main Program Logic
// ============================================================================
// Step 1: Calculate the order total
OrderTotal = CalculateOrderTotal();
// Step 2: Validate customer credit limit
CreditApproved = ValidateCustomerCredit(OrderTotal);
// Step 3: Process order if credit approved
If CreditApproved;
// Check availability for each item
If CheckItemAvailability('WIDGET001': 5);
If CheckItemAvailability('GADGET002': 2);
// All items available, print order total
Write TOTALREC;
EndIf;
EndIf;
Else;
// Credit limit exceeded
Write NOCREDIT;
EndIf;
*InLR = *On;
Return;
// ============================================================================
// Subprocedure: CalculateOrderTotal
// Purpose: Calculate the total amount for all order lines
// Returns: Total order amount as Packed(9:2)
// Notes: In production, this would read from an order file
// For demo purposes, uses hardcoded order lines
// ============================================================================
Dcl-Proc CalculateOrderTotal;
Dcl-Pi *N Packed(9:2) End-Pi;
Dcl-S Total Packed(9:2) Inz(0);
Dcl-S LineAmount Packed(9:2);
// Process first order line - WIDGET001
OrderLine.Item = 'WIDGET001';
OrderLine.Qty = 5;
OrderLine.Price = 19.95;
LineAmount = OrderLine.Qty * OrderLine.Price;
Total += LineAmount;
// Process second order line - GADGET002
OrderLine.Item = 'GADGET002';
OrderLine.Qty = 2;
OrderLine.Price = 49.50;
LineAmount = OrderLine.Qty * OrderLine.Price;
Total += LineAmount;
Return Total;
End-Proc;
// ============================================================================
// Subprocedure: ValidateCustomerCredit
// Purpose: Validate if order total is within customer credit limit
// Parameters: pOrderTotal - The total amount to validate (Const)
// Returns: *On if approved, *Off if credit limit exceeded
// Notes: In production, credit limit would be retrieved from customer master
// For demo purposes, uses hardcoded credit limit constant
// ============================================================================
Dcl-Proc ValidateCustomerCredit;
Dcl-Pi *N Ind;
pOrderTotal Packed(9:2) Const;
End-Pi;
Dcl-C CREDIT_LIMIT Const(250.00);
Dcl-S Approved Ind Inz(*Off);
If pOrderTotal <= CREDIT_LIMIT;
Approved = *On;
EndIf;
Return Approved;
End-Proc;
// ============================================================================
// Subprocedure: CheckItemAvailability
// Purpose: Check if requested quantity is available in inventory
// Parameters: pItem - Item code to check (Const)
// pQuantity - Quantity needed (Const)
// Returns: *On if available, *Off if insufficient inventory
// Notes: In production, this would query inventory database
// For demo purposes, uses hardcoded inventory levels
// Writes NOITEM record if item not available
// ============================================================================
Dcl-Proc CheckItemAvailability;
Dcl-Pi *N Ind;
pItem Char(10) Const;
pQuantity Packed(3:0) Const;
End-Pi;
Dcl-S OnHand Packed(5:0);
Dcl-S Available Ind Inz(*Off);
// In real application, this would query inventory database
// For demo, using hardcoded values matching original logic
Select;
When pItem = 'WIDGET001';
OnHand = 10;
When pItem = 'GADGET002';
OnHand = 1;
Other;
OnHand = 0;
EndSl;
If pQuantity <= OnHand;
Available = *On;
Else;
// Item not available - write error record
Write NOITEM;
EndIf;
Return Available;
End-Proc;
// ============================================================================
// Output specifications (printer file records)
// ============================================================================
// Note: These would typically be in a separate DDS or DSPF file
// Shown here for completeness of the example
Modernised code is often longer in length, but I’m sure you will agree – it’s much cleaner to read.
Cohesive Code is also ripe code to be plucked and placed into a re-usable service program so it can be shared across the entire sales-order application.
A quick litmus test I use all the time
Before writing the sub-procedure I always ask myself:
- Can I describe this procedure’s purpose in one sentence?
- Would someone be surprised by anything it does?
- If I removed this procedure from the program, would its purpose still make sense on its own?
- Could I rewrite this in SQL or Node.js without changing its conceptual responsibility?
If the answer to any of these is “no”, the responsibility isn’t clear enough.
Here is the part that hurts: cohesion is not something you design once at the start of a project and then forget about. It is a decision you make every single time you touch a procedure.
The dangerous thought is always the same: “I will just add this one extra step here because I already have the data loaded.” Sometimes that is fine. More often it is the first tiny crack that eventually splits your nice clean procedure in half.
Next time you hear that voice, pause and ask yourself the uncomfortable question: “Is this still the same responsibility?” If the answer is no, take the slightly longer route and create a new, separate and cohesive sub-procedure. Your future self, and the poor soul who inherits the code when you’ve gone, will thank you.
Until next time, keep your procedures focused, your service programs coherent, and your RPG code maintainable.

