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.
For the module types and the service program objects behind this exercise, see the RPGLE Modules and NOMAIN Reference and RPGLE Service Programs Reference.
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
Thanks