In the first post we covered free-format RPGLE and ended with a subprocedure example. You saw how dcl-proc lets you break logic into a named, callable block inside a single program.
That is a good start. But what happens when you need that same CalcTax logic in fifteen different programs? Do you copy-paste it into each one? Maintain fifteen copies every time the tax rules change?
That is where service programs come in. A service program is a compiled library of subprocedures that any program on the system can call — write the logic once, use it everywhere, update it in one place.
This post covers both: how to write proper subprocedures, and how to package them into a service program your entire application can share.
Subprocedures — a proper look
You already saw a basic subprocedure in the last post. Let us look at the full anatomy before we move to service programs.
**FREE
ctl-opt dftactgrp(*no) actgrp(*caller);
// ── Main procedure ───────────────────────────────
dcl-s GrossAmount packed(11:2);
dcl-s DiscountPct packed(5:4);
dcl-s FinalAmount packed(11:2);
GrossAmount = 12500.00;
DiscountPct = 0.15;
FinalAmount = ApplyDiscount(GrossAmount: DiscountPct);
dsply %char(FinalAmount);
*inlr = *on;
return;
// ── Subprocedure ─────────────────────────────────
dcl-proc ApplyDiscount;
dcl-pi *n packed(11:2);
pAmount packed(11:2) value;
pDisc packed(5:4) value;
end-pi;
dcl-s DiscountAmt packed(11:2);
DiscountAmt = pAmount * pDisc;
return pAmount - DiscountAmt;
end-proc;Three things to understand here:
dcl-pi is the procedure interface — it defines what the procedure accepts (parameters) and what it returns. The *n means the return value has no name, just a type. You can also write dcl-pi ApplyDiscount if you prefer the name explicit.
value on the parameters means the procedure gets a copy — changes inside the procedure do not affect the caller’s variable. Remove value and the parameter is passed by reference, meaning changes inside DO affect the caller.
dcl-s inside dcl-proc is local — those variables only exist while the procedure is running. Once it returns they are gone. Use this intentionally; local variables are cleaner and safer than globals.
Returning multiple values
A procedure can only return one value directly. If you need to return multiple things, use a data structure parameter passed by reference:
dcl-ds PayrollResultDS qualified;
NetPay packed(11:2);
TaxAmount packed(9:2);
Bonus packed(9:2);
end-ds;
dcl-proc CalcPayroll;
dcl-pi *n ind;
pSalary packed(11:2) value;
pResult likeds(PayrollResultDS); // passed by reference
end-pi;
if pSalary <= 0;
return *off; // return false = error
endif;
pResult.TaxAmount = pSalary * 0.25;
pResult.Bonus = pSalary * 0.10;
pResult.NetPay = pSalary - pResult.TaxAmount + pResult.Bonus;
return *on; // return true = success
end-proc;The procedure returns an indicator (ind) as a success/failure flag, and fills the data structure with the real results. This is a common IBM i pattern — clean and easy to extend.
What is a service program
A service program (*SRVPGM) is a compiled object that contains subprocedures, data, and optionally file access — but no main procedure and no entry point. It cannot be called directly like a program. Instead, other programs bind to it and call its procedures as if they were their own.
Think of it like a DLL on Windows or a shared library on Linux. The logic lives in one place, compiled once, called by many.
To build one you need three source members:
QSRVSRC/TAXSRV.binder ← binder language (exports)
QRPGLESRC/TAXSRV.rpgle ← the subprocedures
QRPGLESRC/TAXSRVH.rpgle ← prototype definitions (the header)Step 1 — Write the prototype header
The header file (TAXSRVH) contains only dcl-pr definitions — prototypes that tell calling programs what procedures exist and what they expect. No actual code, just declarations:
**FREE
// ── Prototypes for TAXSRV service program ────────────
// Include this header in any program that calls TAXSRV
dcl-pr CalcIncomeTax packed(9:2) extproc('CalcIncomeTax');
pSalary packed(11:2) value;
pYear int(4) value;
end-pr;
dcl-pr GetTaxBracket char(10) extproc('GetTaxBracket');
pSalary packed(11:2) value;
end-pr;
dcl-pr IsExempt ind extproc('IsExempt');
pEmpID char(6) value;
end-pr;Step 2 — Write the service program source
The main source file (TAXSRV) contains the actual procedure implementations. Notice there is no main procedure and no *inlr:
**FREE
ctl-opt dftactgrp(*no) actgrp(*caller) nomain;
/copy QRPGLESRC,TAXSRVH // include our own header
// ── CalcIncomeTax ────────────────────────────────────
dcl-proc CalcIncomeTax export;
dcl-pi *n packed(9:2);
pSalary packed(11:2) value;
pYear int(4) value;
end-pi;
dcl-s TaxRate packed(5:4);
dcl-s TaxAmt packed(9:2);
if pSalary > 1000000;
TaxRate = 0.30;
elseif pSalary > 500000;
TaxRate = 0.25;
elseif pSalary > 250000;
TaxRate = 0.20;
else;
TaxRate = 0.10;
endif;
TaxAmt = pSalary * TaxRate;
return TaxAmt;
end-proc;
// ── GetTaxBracket ─────────────────────────────────────
dcl-proc GetTaxBracket export;
dcl-pi *n char(10);
pSalary packed(11:2) value;
end-pi;
if pSalary > 1000000;
return '30%';
elseif pSalary > 500000;
return '25%';
elseif pSalary > 250000;
return '20%';
else;
return '10%';
endif;
end-proc;
// ── IsExempt ──────────────────────────────────────────
dcl-proc IsExempt export;
dcl-pi *n ind;
pEmpID char(6) value;
end-pi;
dcl-s ExemptList char(6) dim(3) ctdata;
if %lookup(pEmpID: ExemptList) > 0;
return *on;
endif;
return *off;
end-proc;
**CTDATA ExemptList
EMP001
EMP002
EMP099The keyword export on each dcl-proc is what makes the procedure visible outside the service program. Without export, the procedure is private — callable only within the service program itself.
The nomain keyword in ctl-opt tells the compiler this module has no main procedure — it is a pure library.
Step 3 — Write the binder language source
The binder source (TAXSRV in QSRVSRC) defines which procedures are exported from the service program and controls versioning. This is what IBM i uses to know what the service program publicly exposes:
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('TAXSRV_V1')
EXPORT SYMBOL('CalcIncomeTax')
EXPORT SYMBOL('GetTaxBracket')
EXPORT SYMBOL('IsExempt')
ENDPGMEXPStep 4 — Compile it
Run these commands in order from a command line or STRSQL session:
// 1. Compile the module (not a program — a module)
CRTRPGMOD MODULE(MYLIB/TAXSRV)
SRCFILE(MYLIB/QRPGLESRC)
SRCMBR(TAXSRV)
DBGVIEW(*SOURCE)
// 2. Create the service program from the module
CRTSRVPGM SRVPGM(MYLIB/TAXSRV)
MODULE(MYLIB/TAXSRV)
SRCFILE(MYLIB/QSRVSRC)
SRCMBR(TAXSRV)
EXPORT(*SRCFILE)If it compiles without errors you now have a *SRVPGM object called TAXSRV in MYLIB.
Step 5 — Call it from another program
Any program that wants to use the service program just includes the header and binds to it at compile time:
**FREE
ctl-opt dftactgrp(*no) actgrp(*caller);
/copy QRPGLESRC,TAXSRVH // pull in the prototypes
dcl-s EmployeeSalary packed(11:2);
dcl-s TaxOwed packed(9:2);
dcl-s Bracket char(10);
EmployeeSalary = 750000;
// Call service program procedures just like local ones
TaxOwed = CalcIncomeTax(EmployeeSalary: 2025);
Bracket = GetTaxBracket(EmployeeSalary);
dsply ('Tax: ' + %char(TaxOwed));
dsply ('Bracket: ' + %trimr(Bracket));
*inlr = *on;
return;When you compile this calling program, you bind it to the service program:
CRTBNDRPG PGM(MYLIB/MYPGM)
SRCFILE(MYLIB/QRPGLESRC)
SRCMBR(MYPGM)
BNDSRVPGM(MYLIB/TAXSRV)Updating a service program without recompiling callers
This is the real power of service programs. If you need to fix a bug in CalcIncomeTax, you recompile only the service program. All programs already bound to it pick up the change automatically — no recompile needed, as long as you have not changed the signature.
If you ADD a new procedure to the service program, update the binder source like this:
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('TAXSRV_V2')
EXPORT SYMBOL('CalcIncomeTax')
EXPORT SYMBOL('GetTaxBracket')
EXPORT SYMBOL('IsExempt')
EXPORT SYMBOL('CalcSurchargeTax') // new procedure
ENDPGMEXP
STRPGMEXP PGMLVL(*PRV) SIGNATURE('TAXSRV_V1')
EXPORT SYMBOL('CalcIncomeTax')
EXPORT SYMBOL('GetTaxBracket')
EXPORT SYMBOL('IsExempt')
ENDPGMEXPKeeping the previous signature (*PRV) means older programs bound to V1 still work without recompiling. New programs can bind to V2 and access the new procedure. This is how IBM i maintains backward compatibility across large application suites.
When to use a service program vs a standalone program
Use a service program when the logic is reusable across multiple programs — validation routines, calculation engines, formatting utilities, database access layers, API wrappers.
Use a standalone program when the logic is specific to one business function, has a clear entry and exit, or needs to be called from a menu or job queue.
In a well-structured IBM i application you will typically have a thin layer of programs (called from menus or batch jobs) that delegate almost all real work to service programs. The programs orchestrate; the service programs do the work.
What to build next
Take your CalcTax subprocedure from the first post and turn it into a service program. Add two more related procedures — maybe CalcNI and CalcNetPay. Write a simple calling program that uses all three.
That exercise will make the compile → bind → call cycle click in a way that no amount of reading will.
The next post covers embedded SQL in RPGLE — how to run DB2 queries directly inside your RPG code, handle cursors, and avoid the mistakes that cause performance problems on busy systems.
Good Article