☕ Caffeine Overload: When RPG Programs Do Too Much
Today's Coffee: Death Wish Coffee — billed as the world's strongest brew. Bold. Intense. Not for the faint of heart. Like overloaded RPG programs, it promises raw power, but abuse it and you're left with jitters, crashes, and regret.
The Monolithic RPG Program: A Death
Wish in Code
Every IBM i shop has one (or ten):
- 6,000+ lines of tangled RPG, half
fixed-form, half free-form.
- Indicators 01–99 flickering like
a Christmas tree.
- Database reads, writes, and
updates scattered between screen handling and report logic.
- Business rules from the 1990s
patched with quick fixes in 2005 and "just one more IF" in 2020.
It runs, sure. But every time you touch it, your developers whisper:
"This program is cursed."
Like a double Death Wish espresso before midnight — it feels like power,
but it's really just technical debt in disguise.
The Anatomy of a Monster Program
Let me paint you a picture. You inherit a program called CUSTMAINT. Innocent enough name, right? Wrong.
This beast handles:
- Customer maintenance screens (3
different formats)
- Order entry and validation
- Credit limit checking and
approval workflows
- Invoice generation and printing
- Email notifications to sales reps
- Automated backup customer
creation
- Historical reporting (last 18
months of activity)
- Integration with the shipping
system
- Tax calculation for 12 different
jurisdictions
- Loyalty points calculation and
redemption
The kicker? It's a single source member with 47 subroutines, 23 copybook includes,
and comments like "// TODO: Fix this later - GV 03/15/2018"
Sound familiar? That's because every IBM i developer has met this
program's cousin, brother, or evil twin.
Why RPG Monoliths Still Exist
1. Legacy Mindset
Old RPG II/III days demanded cramming everything into one program. Memory
and disk were expensive, so efficiency meant monoliths. Back then, a 64K
program was huge. Today's systems laugh at 64K, but our programming habits
haven't evolved.
2. Shortcut Culture
Adding an IF statement or an extra subroutine was faster than designing
new modules. "I'll just add this one little feature..." becomes the
famous last words of clean architecture.
3. The Hero Developer Problem
"Only Bob understands this program" becomes an excuse for not
refactoring. Bob becomes a single point of failure, and when Bob retires,
you're left with archaeological code excavation.
4. Fear of Change
"If it compiles, don't touch it." Until it doesn't. Then you're
debugging at 2 AM wondering why changing a display field broke the invoice
calculation routine.
5. Deadline Pressure
Business needs features yesterday. Clean design takes time. Duct tape and
bubble gum take minutes. Guess which one wins when the VP is breathing down
your neck?
Warning Signs: How to Spot "Death
Wish" RPG
Compilation Red Flags
- The compile takes so long you
actually do make another pot of coffee.
- The listing is longer than a
Stephen King novel.
- You need to increase your job's
memory allocation just to compile it.
Developer Behavior
- Developers fight over who has to
debug it.
- New team members avoid it like
expired milk.
- Senior developers make
"sacrifice to the code gods" jokes before touching it.
- Code reviews become group therapy
sessions.
Functional Chaos
- The program updates customers,
creates invoices, prints reports, and emails sales reps — all in the same
cycle.
- Adding a single field means
tracing 500 lines just to avoid breaking something.
- Error messages like
"Something went wrong in routine XYZ" tell you nothing.
- The program has three different
ways to do the same calculation.
Business Impact
- Simple changes take weeks instead
of hours.
- Testing requires a PhD in program
archaeology.
- Onboarding new developers becomes
a six-month commitment.
- Business rules are buried so deep
that domain experts can't validate them.
How to Fix It: Brew Small, Brew Strong
Death Wish coffee is powerful because it's brewed right, not because it's
dumped into one massive pot. RPG code should be the same:
1. Separate Concerns
Database Access: Move into service programs or dedicated procedures.
// Instead of scattered CHAINs throughout your program
dcl-pr getCustomerData likeds(customer_t);
custId
packed(7: 0) const;
end-pr;
dcl-pr updateCustomerStatus ind;
custId packed(7:
0) const;
newStatus
char(1) const;
end-pr;
Business Logic: Keep pure — no UI or report clutter.
// Clean business rules, testable and reusable
dcl-pr calculateDiscountPercent packed(5: 2);
orderTotal
packed(11: 2) const;
customerType
char(10) const;
loyaltyPoints
packed(9: 0) const;
end-pr;
Presentation: Screens, APIs, reports — separate layer.
// UI concerns isolated from business logic
dcl-pr displayCustomerScreen;
customer
likeds(customer_t) const;
mode char(10)
const; // ADD, CHANGE, DISPLAY
end-pr;
2. Refactor to Free-Form
Converting from fixed-form forces clarity. You'll find dead code,
duplicate logic, and hidden assumptions. Free-form syntax supports modular
design — procedures, subprocedures, clean parameters.
Before (Fixed-Form Madness):
C CustId CHAIN CUSTFILE
C
IF %FOUND(CUSTFILE)
C
EVAL WS_Name = CF_Name
C
EVAL WS_Addr = CF_Addr
C
OrderId CHAIN ORDERFILE
C
IF %FOUND(ORDERFILE)
C
EVAL WS_Total = OF_Total
C
ENDIF
C
ENDIF
After (Free-Form Clarity):
customer = getCustomer(custId);
if customer.id > 0;
orders =
getCustomerOrders(custId);
displayCustomerDetail(customer : orders);
endif;
3. Use SQL for Set Logic
Don't loop through 100,000 records with READE. Replace nested CHAINs with
JOINs and aggregations.
Before (Loop of Death):
// Reading every invoice to calculate customer totals
setll custId INVFILE;
reade custId INVFILE;
dow not %eof(INVFILE);
totalDue +=
INV_Amount;
if INV_Status =
'OVERDUE';
overdueCount
+= 1;
endif;
reade custId
INVFILE;
enddo;
After (SQL Power):
exec SQL
SELECT
COALESCE(SUM(INV_AMOUNT), 0),
COALESCE(SUM(CASE WHEN INV_STATUS = 'OVERDUE'
THEN 1 ELSE 0 END),
0)
INTO
:totalDue, :overdueCount
FROM INVFILE
WHERE
INV_CUSTID = :custId;
4. Modularize with Service Programs
Customer utilities in one service program:
// CUSTSRV - Customer Service Program
dcl-pr validateCustomer ind export;
customer
likeds(customer_t) const;
end-pr;
dcl-pr calculateCreditLimit packed(11: 2) export;
custId
packed(7: 0) const;
end-pr;
Orders in another:
// ORDSRV - Order Service Program
dcl-pr createOrder likeds(order_t) export;
custId
packed(7: 0) const;
items
likeds(orderItem_t) dim(999) const;
end-pr;
Logging and error handling in a reusable module:
// LOGSRV - Logging Service Program
dcl-pr logError export;
program
char(10) const;
procedure
char(30) const;
message
char(256) const;
end-pr;
This makes code testable, portable, and easier to maintain.
Real-World Refactoring Strategy
Phase 1: Stop the Bleeding
- No new features in the monolith
- All new functionality goes into
separate modules
- Establish coding standards for
new components
Phase 2: Extract and Isolate
- Move database I/O into service
programs
- Extract business calculations
into pure functions
- Create dedicated error handling
Phase 3: Replace Core Functions
- Rebuild critical paths with clean
architecture
- Migrate data validation to
service programs
- Implement comprehensive logging
Phase 4: Sunset the Monster
- Route new transactions through
modern components
- Keep legacy for historical data
only
- Plan retirement ceremony
(seriously, celebrate wins)
Real-World Before/After
Before (Monolithic RPG):
C READ CustFile
C
IF NOT %EOF
C CustId CHAIN
CustOrders
C
IF %FOUND
C
EXFMT CustScreen
C
UPDATE CustFile
C
WRITE Invoice
C
CALL 'EmailNotify'
C
ENDIF
C
ENDIF
After (Decomposed RPG + SQL):
// Get customer data
custData = getCustomer(custId);
if custData.status = 'ACTIVE';
// Get orders
with SQL
orders =
getOrdersByCustomer(custId);
// Apply
business logic
discount =
calculateDiscount(custData : orders);
// Process
transaction
invoice =
createInvoice(custId : orders : discount);
// Handle
notifications
callp
notifyCustomerService(custId : invoice);
callp
logCustomerAccess(custId);
endif;
Business Impact:
- Before: 47 lines, 15 active
indicators, 3 developers afraid to touch it
- After: 12 lines, clear intent,
testable components, junior developers can contribute
The Testing Problem (And Solution)
Monolithic programs are testing nightmares. How do you unit test a
6,000-line program that reads files, updates databases, sends emails, and
prints reports?
Answer: You can't. But you can test modular components:
// Testable business logic
dcl-pr test_calculateDiscount;
end-pr;
dcl-proc test_calculateDiscount;
dcl-s customer
likeds(customer_t);
dcl-s
expectedDiscount packed(5: 2);
dcl-s
actualDiscount packed(5: 2);
// Arrange
customer.type =
'PREMIUM';
customer.loyaltyPoints = 5000;
expectedDiscount = 15.00;
// Act
actualDiscount
= calculateDiscount(customer);
// Assert
assert(actualDiscount = expectedDiscount :
'Premium
customer discount failed');
end-proc;
Clean modules enable automated testing, which enables confident
refactoring, which enables sustainable development.
The Manager's Role: Stop Brewing
Monsters
Developers don't set standards — managers do. If you allow "just one
more fix" in the giant monolith, you're approving technical debt at scale.
Code Reviews: Block PRs that add to
the monster.
Insist on modularization. Make it clear that adding to technical debt
requires explicit approval and documented business justification.
Refactoring Time: Budget it.
Technical debt is business risk. Schedule "architecture weeks"
where teams focus solely on decomposing monoliths. Track velocity improvements
and reduced bug counts.
Standards: Enforce patterns for I/O,
error handling, and program size.
- Maximum program size: 1,000 lines
- Dedicated service programs for
all database access
- Standardized error handling and
logging
- Mandatory code reviews for
programs over 500 lines
Coaching: Teach juniors why smaller
programs scale better.
Instead of letting them repeat bad habits, show them the business impact
of clean architecture. Pair them with senior developers during refactoring
exercises.
Metrics That Matter:
- Time to implement new features
- Bug count per 1,000 lines of code
- Developer onboarding time
- Code coverage in automated tests
Common Refactoring Mistakes (And How
to Avoid Them)
Mistake 1: Big Bang Rewrite
Wrong: "Let's rewrite the entire CUSTMAINT program from scratch." Right:
Extract one function at a time, test thoroughly, then move to the next.
Mistake 2: Over-Engineering
Wrong: Creating 47 service programs for a simple customer maintenance function.
Right: Start with clear separation of concerns, then optimize based on
actual complexity.
Mistake 3: No Migration Plan
Wrong: Building new components without a path to retire legacy code. Right:
Plan the sunset from day one. Legacy code should have an expiration date.
Mistake 4: Ignoring Performance
Wrong: Assuming modular code is automatically slower. Right: Profile
before and after. Clean SQL often outperforms nested loops by orders of
magnitude.
Final Sip
Death Wish Coffee is marketed as the world's strongest brew. One cup is
bold. Two cups is a thrill. But guzzling it nonstop? That's a death wish.
RPG programs are the same. A powerful utility is great. A monolith that
tries to handle everything? That's a recipe for developer burnout, onboarding
failure, and technical debt that compounds like interest on a credit card.
The strongest programs aren't the biggest ones. They're the ones that do
one thing exceptionally well, play nicely with others, and can be understood by
developers who didn't write them.
☕ Keep your RPG lean. Brew small. Brew strong.
Don't let your codebase crash harder than a triple espresso at midnight.
Your future self (and your team) will thank you for choosing clarity over
complexity, modularity over monoliths, and sustainable architecture over
short-term fixes.
Because in the end, the best programs aren't the ones that impress with
their size — they're the ones that deliver business value without giving anyone
nightmares.
Follow The RPG Blend for spicy insights on RPG modernization, AI in the IBM i shop, and
leadership strategies that actually work.
Comments
Post a Comment