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.

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.

1 thought 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