Subprocedures and Service Programs in RPGLE: write once, call everywhere

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
EMP099

The 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')
ENDPGMEXP

Step 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')
ENDPGMEXP

Keeping 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.

2 thoughts on “Subprocedures and Service Programs in RPGLE: write once, call everywhere”

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top