Strangler Fig Pattern for IBM i Application Modernization: Decomposing Monolithic RPG, API Gateway Routing, Incremental Migration, and Coexistence Strategies in 2026



The previous post covered building a REST API server on IBM i with Node.js and Express — installing Node.js in PASE, connecting to DB2 for i with the odbc package, defining route handlers, adding API key authentication middleware, returning paginated JSON responses, centralised error handling, and running the Node.js server as a persistent IBM i batch job submitted via SBMJOB. This post covers the strangler fig pattern for IBM i application modernization: what the pattern is and why it suits IBM i, identifying functional seams in monolithic RPG applications, building a REST facade over legacy RPG program calls using Node.js or Nginx, routing traffic incrementally from the legacy system to the new implementation, maintaining DB2 for i as the shared data layer during coexistence, and planning a phased full-cutover strategy in 2026.

What Is the Strangler Fig Pattern?

The strangler fig is a software modernization pattern named after the tropical fig tree that grows around a host tree, gradually taking over its structure until the original tree dies and the fig stands independently. Applied to software, it means:

  1. Do not rewrite the entire system at once. A “big bang” rewrite has a poor track record — years of effort, high risk, and a freeze on new features during the migration period.
  2. Identify a seam — a discrete function or module that can be replaced independently without breaking the rest of the system.
  3. Build the replacement alongside the legacy system, not instead of it.
  4. Route traffic for that function to the new implementation via a facade or API gateway.
  5. Retire the legacy code for that seam once the new path is proven in production.
  6. Repeat seam by seam until the legacy system is fully replaced.

For IBM i, the pattern is particularly well-suited because the legacy RPG programs are extremely stable and continue running correctly during migration — there is no need to freeze features or accept increased defect rates during a lengthy rewrite.

Identifying Seams in a Monolithic RPG Application

A seam is a boundary where you can separate behaviour without changing the surrounding code. Good seam candidates on IBM i:

  • Outbound data feeds — if an RPG program produces a flat file or calls SNDDST to email a report, replace that output step with a REST call to a new microservice. The RPG business logic stays unchanged.
  • Lookup / reference data queries — pricing lookups, product catalogue reads, and currency conversion are stateless queries with no side effects. These are the easiest first seams.
  • External integrations — EDI translation, bank file formatting, and logistics provider APIs are already at the system boundary. Replace them one at a time.
  • Screen-specific logic — a specific 5250 screen that handles one business function (e.g., customer credit limit check) can be replaced with a browser-based form backed by a new service, while all other screens continue to use the legacy RPG path.
/* Original monolith: ORDLIB/ORDENTRY calls ORDLIB/CREDITCK inline */
/* Seam identified: credit limit check — stateless, DB2-read-only, high-volume */

/* Step 1: Extract CREDITCK into a service program with a clean interface */
/* Step 2: Build a new REST microservice that implements the same logic */
/* Step 3: Route credit check calls to the new service via facade */
/* Step 4: Retire ORDLIB/CREDITCK once new service is proven */

Building a REST Facade Over Legacy RPG: The Facade Layer

The facade is a thin HTTP layer that sits in front of both the legacy RPG path and the new service. Clients (5250 screens, batch jobs, or new browser UIs) call the facade URL. The facade decides which backend to route to based on a feature flag, a percentage split, or a simple cutover switch:

// facade/routes/creditCheck.js — Node.js facade on IBM i PASE
'use strict';

const express  = require('express');
const router   = express.Router();
const db       = require('../db');
const rpgCall  = require('../lib/rpgCall');   // XMLSERVICE wrapper

// Feature flag: read from APPLIB/FEATUREFLAGS data area or DB2 table
async function useNewCreditService() {
  const rows = await db.query(
    `SELECT FLAG_VALUE FROM APPLIB.FEATURE_FLAGS WHERE FLAG_NAME = 'NEW_CREDIT_SVC'`
  );
  return rows.length > 0 && rows[0].FLAG_VALUE === '1';
}

// POST /api/credit-check  { custId, orderAmt }
router.post('/', async (req, res, next) => {
  try {
    const { custId, orderAmt } = req.body;

    if (await useNewCreditService()) {
      // New implementation: direct DB2 query
      const rows = await db.query(
        `SELECT CREDIT_LIMIT, OUTSTANDING_BAL,
                CREDIT_LIMIT - OUTSTANDING_BAL AS AVAILABLE_CREDIT
         FROM   APPLIB.CUSTOMERS
         WHERE  CUST_ID = ?`,
        [custId]
      );
      if (rows.length === 0) return res.status(404).json({ error: 'Customer not found' });
      const { CREDIT_LIMIT, OUTSTANDING_BAL, AVAILABLE_CREDIT } = rows[0];
      const approved = orderAmt <= AVAILABLE_CREDIT;
      return res.json({ approved, availableCredit: AVAILABLE_CREDIT, reason: approved ? 'OK' : 'LIMIT_EXCEEDED' });
    }

    // Legacy path: call RPG CREDITCK via XMLSERVICE
    const result = await rpgCall('ORDLIB', 'CREDITCK', [
      { name: 'CUSTID',   type: '7p0', value: custId   },
      { name: 'ORDAMT',   type: '13p2', value: orderAmt },
      { name: 'APPROVED', type: '1a',  inout: 'out'    },
      { name: 'REASON',   type: '10a', inout: 'out'    }
    ]);
    res.json({ approved: result.APPROVED === '1', reason: result.REASON.trim() });
  } catch (err) { next(err); }
});

module.exports = router;

Feature Flag Table for Incremental Traffic Routing

A DB2 table of feature flags allows operations to switch individual seams without a deployment. The facade reads the flag at request time — no restart required:

-- Create the feature flags table
CREATE TABLE APPLIB.FEATURE_FLAGS (
  FLAG_NAME    CHAR(30)     NOT NULL,
  FLAG_VALUE   CHAR(1)      NOT NULL DEFAULT '0',   -- '0'=legacy, '1'=new
  DESCRIPTION  VARCHAR(100),
  UPDATED_BY   CHAR(10),
  UPDATED_TS   TIMESTAMP    NOT NULL DEFAULT CURRENT TIMESTAMP,
  CONSTRAINT PK_FEATUREFLAGS PRIMARY KEY (FLAG_NAME)
);

-- Initial values: all flags off (all traffic to legacy)
INSERT INTO APPLIB.FEATURE_FLAGS (FLAG_NAME, FLAG_VALUE, DESCRIPTION)
VALUES
  ('NEW_CREDIT_SVC',   '0', 'Credit limit check: 0=RPG CREDITCK, 1=new DB2 service'),
  ('NEW_PRICING_SVC',  '0', 'Pricing lookup: 0=RPG PRICECALC, 1=new REST service'),
  ('NEW_INVENTORY_SVC','0', 'Stock check: 0=RPG STCKCHK, 1=new DB2 service');

-- Enable new credit service (flip the switch — no deployment needed)
UPDATE APPLIB.FEATURE_FLAGS
SET    FLAG_VALUE = '1', UPDATED_BY = 'DEVUSER', UPDATED_TS = CURRENT TIMESTAMP
WHERE  FLAG_NAME  = 'NEW_CREDIT_SVC';

Nginx as an API Gateway for Percentage-Based Traffic Splitting

When the new service is ready for gradual rollout, Nginx (running in PASE) can split traffic by percentage between the legacy facade and the new service. Start at 5%, monitor error rates, and increase to 100% over days or weeks:

# /etc/nginx/conf.d/ordapi.conf — Nginx upstream split

upstream credit_check_backend {
    # 95% to legacy Node.js facade (calls RPG CREDITCK)
    server 127.0.0.1:3000 weight=95;
    # 5% to new credit microservice
    server 127.0.0.1:3100 weight=5;
}

server {
    listen 8080;

    location /api/credit-check {
        proxy_pass         http://credit_check_backend;
        proxy_set_header   X-Forwarded-For  $remote_addr;
        proxy_set_header   Host             $host;
        proxy_read_timeout 30s;
    }

    # All other /api routes go to the main facade
    location /api/ {
        proxy_pass  http://127.0.0.1:3000;
    }
}

DB2 for i as the Shared Data Layer During Coexistence

The key to strangler fig on IBM i is keeping DB2 for i as the single source of truth throughout the migration. Both the legacy RPG programs and the new microservices read from and write to the same DB2 tables. This eliminates data synchronisation complexity — there is no two-phase commit or event sourcing required during the migration phase:

  • Legacy RPG programs continue to use native I/O (CHAIN, READ, WRITE) or embedded SQL to DB2 tables in ORDLIB, APPLIB, etc.
  • New microservices use the odbc package (Node.js) or psycopg2-ibm-db (Python) to connect to the same DB2 tables.
  • Schema changes are coordinated: add nullable columns for the new service, keep existing columns for legacy programs, and deprecate legacy columns only after legacy programs are retired for that table.

Seam Migration Checklist

PhaseActivitySuccess Criterion
1. ExtractIdentify seam, document RPG program interface (parameters, DB2 tables)Interface specification signed off
2. BuildImplement new service, write automated tests against shared DB2 dataAll tests pass; parity with legacy output confirmed
3. FacadeDeploy facade, route 0% to new service (feature flag OFF)All traffic via legacy; no regressions
4. CanaryRoute 5% of traffic to new service, compare resultsError rate and latency within tolerance
5. RampIncrease to 25%, 50%, 100% over successive deploymentsZero escalated incidents at each step
6. RetireDelete legacy RPG program and feature flag entryLegacy program absent from production library

Next post: IBM i TCP/IP configuration and troubleshooting — using the CFGTCP menu, adding and removing IP interfaces with ADDTCPIFC and RMVTCPIFC, configuring DNS with CHGTCPDMN, diagnosing network connectivity with NETSTAT, PING, and TRACEROUTE, setting up virtual Ethernet between LPARs, and managing Ethernet line descriptions on IBM i in 2026.

Leave a Comment

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

Scroll to Top