For decades, legacy RPG systems have been carrying out tasks successfully, including but not limited to transactions, making payments, and tracking inventory.
By JoydipKanjilal
This article examines modern enterprise architecture strategies that any IBM i shop can apply to its advantage.
Layered architecture: Isolating Responsibilities
Often, applications mix business logic, data access logic, transaction management, and validation logic in the same layer. While this can be convenient for application developers, this approach is not recommended for many reasons. For example, when the components in your application are entangled, you cannot reuse the components in another layer of the application or in any other application.
A typical business application can be divided into presentation, application, domain, and infrastructure layers as shown in Figure 1.
Figure 1: Typical layers of a business application
Moreover, this approach hinders testability because tightly coupled components are extremely difficult to test individually. Hence, the solution is a layered approach that isolates responsibilities in an application using logical boundaries. A typical approach for separating responsibilities can comprise the following layers:
- Adapters - this layer acts as a bridge between two incompatible interfaces, enabling them to communicate.
- Application services - this is used to manage a business process, ensuring authorization, ownership of the transaction, and calling other functionalities.
- Business logic - this is the layer that is used to implement the business rules of your application.
- Infrastructure - this is the layer that provides persistence, messaging, and facilitates communication with internal and external services.
Figure 2 illustrates a typical layered IBM i architecture.
Figure 2: Illustrating a layered IBM i architecture
An inbound adapter gets requests through a user interface, or from the presentation layer, while the outbound adapter connects to other databases and/or services offered by the infrastructure layer. Validation works similarly. For instance, when the .NET Order API encounters an improperly formatted request, such as one with no content, it rejects it and returns an HTTP status code of 400.
Service boundaries: Organizing your application architecture around business capabilities
Service boundaries are logical limits that define the capabilities and functionalities of a specific service together with the data and APIs it has access to. You should create service boundaries that define the business responsibilities rather than the technical implementations of a particular operation.
For example, the function “placeOrder,” “updateStock,” or “sendShipment indicates the intent of a particular operation, while a function “updateFile” does not provide enough information on the actual intent of the operation because you may update a file for various purposes.
Figure 3 illustrates distinct service boundaries in an application.
Figure 3: Illustrating the service boundaries in a typical business application
Every capability shown in Figure 3 has its own data and behavior through specialized contracts, not through the use of shared tables:
- Order Management: This component is the order orchestrator and contains the order service that coordinates the end-to-end order lifecycle without owning domain rules of other services.
- Credit Management: This component is responsible for credit approval and decides if an order can be released based on credit rules.
- Inventory Availability: This component comprises the stock service and warehouse service and owns stock levels and reservations.
- Customer Management: This component comprises the customer identity and profile service and owns customer identity and preferences needed by other services.
- Shipping and Fulfillment: This component comprises the fulfillment, dispatch, and logistics services and executes shipping of a released order and tracks its fulfillment state.
- Notification Service: This component comprises alerting and messaging services and reacts to events and sends notifications via emails and text messages accordingly.
When you’re migrating your existing application to a newer architecture, such as microservices, it is recommended that you start with a modular monolith architecture. When you’re building your services, you should organize the application architecture around business capabilities, and your architecture should be able to address the following questions:
1. What business decisions does it make?
2. What data can the service access and/or update?
3. What data does the service return in case of success and failure?
IBM Integrated Web Services for i provides the chance to expose RPG programs and service programs. Create a wrapper to change transport fields, validate requests, perform error mapping, and call the application service.
The IWS endpoint in this code example acts as an inbound adapter for mapping the JSON request fields to ORDPLACE parameters and then returns the output parameters as JSON. This endpoint takes in a request body that includes: requestId, customerId, itemNumber, and quantity. After the endpoint has processed the request, it will return an HTTP 201 status code and the orderId of the created order.
In the minimalistic simple application that we will discuss in the sections that follow, assume that a customer has ordered one or more items using the Order API built using .NET. The API then sends the order to an Integrated Web Services (IWS) endpoint running on an IBM i.
Next, the endpoint executes the RPG program called ORDPLACE, which in turn uses the credit check service program CRDCHK to verify the credit status of the customer. It also uses the inventory reservation program INVRSV to reserve the in-stock item. Finally, the order is saved in the Db2 for i tables ORDHDR and ORDDTL.
The following code snippet shows the error returned by the IBM i endpoint when the order is rejected.
HTTP/1.1 422
{ "requestId": "50dcbd9e-e4bf-44ea-a6d0-60eaa5318eb7",
"errorCode": "CREDIT_LIMIT_EXCEEDED",
"errorText": "Order exceeds available credit." }
Here is a code example that shows how you can call an IBM i business service from a .NET API:
using System.Net;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient < IOrderGateway, OrderGateway > (
client =>
{
client.BaseAddress = new Uri(
builder.Configuration["IBMi:BaseUrl"] ??
throw new InvalidOperationException("IBM i service URL is missing"));
client.Timeout = TimeSpan.FromSeconds(10);
});
var app = builder.Build();
app.MapPost("/api/orders", async (PlaceOrderCommand command,
IOrderGateway gateway, CancellationToken ct) =>
{
if (command.Quantity <= 0)
return Results.BadRequest("Quantity must be greater than zero.");
try
{
var result = await gateway.PlaceAsync(command, ct);
return Results.Created($ "/api/orders/{result.OrderNumber}", result);
}
catch (OrderRejectedException ex)
{
return Results.Problem(statusCode: 422, title: ex.Error.ErrorText,
extensions: new Dictionary < string, object ? >
{
["errorCode"] = ex.Error.ErrorCode,
["requestId"] = ex.Error.RequestId
});
}
});
app.Run();
public sealed record PlaceOrderCommand(
string RequestId, string CustomerId,
string ItemNumber, int Quantity);
public sealed record PlaceOrderResult(string OrderNumber);
public sealed record IbmiError(
string RequestId, string ErrorCode, string ErrorText);
public sealed class OrderRejectedException(IbmiError error): Exception(error.ErrorText)
{
public IbmiError Error
{
get;
} = error;
}
public interface IOrderGateway
{
Task <PlaceOrderResult> PlaceAsync(
PlaceOrderCommand command, CancellationToken ct);
}
public sealed class OrderGateway: IOrderGateway
{
private readonly HttpClient _http;
public OrderGateway(HttpClient http) => _http = http;
public async Task < PlaceOrderResult > PlaceAsync(
PlaceOrderCommand command, CancellationToken ct)
{
using
var response = await _http.PostAsJsonAsync("orders", command, ct);
if (response.StatusCode == HttpStatusCode.UnprocessableEntity)
{
var error = await response.Content
.ReadFromJsonAsync < IbmiError > (cancellationToken: ct) ??
new IbmiError(command.RequestId, "UNKNOWN", "Order rejected");
throw new OrderRejectedException(error);
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync < PlaceOrderResult > (cancellationToken: ct) ??
throw new InvalidOperationException("IBM i order service returned an empty response");
}
}
In the preceding code listing, the AddHttpClient method is used to register the OrderGateway service with the IBM i endpoint base URL. The MapPost method is the inbound adapter. This method will reject an order if the quantity being ordered is less than or equal to zero and return an HTTP status code of 400.
Valid orders, i.e., those that pass the validation checks, are passed to the gateway. While the method will return an HTTP status code of 201 on success, it will return an HTTP status code of 422 if the gateway throws an OrderRejectedException. This response will contain both the errorCode and requestId that were returned from the IBM i system.
The caller of the API will generate a requestId (a GUID) and send that requestId within the payload of the request. OrderGateway is the outbound adapter. When called, the gateway will check for an HTTP status code of 422 before checking for other error types. Once the gateway has identified that there is an HTTP 422 status code, it will read the business error from the payload and throw an OrderRejectedException.
The EnsureSuccessStatusCode() method cannot be used by itself to process business errors; it is only able to throw HttpRequestException if the HTTP response code is not in the range of 200 through 299. Once this occurs, the exception discards the response body, preventing the caller from receiving the error code and error message generated by ORDPLACE. The EnsureSuccessStatusCode() method will only process those failures that occur after checking for HTTP 422, i.e., authentication failures, incorrect URLs, etc.
If any of these types of failures occur, they will appear to the caller as an HTTP 500 error. However, when processing these errors in a production environment, you should consider converting them to either 502 or 503 errors with the appropriate request IDs attached.
Therefore, if an order is rejected, it should follow the same pattern: CRDCHK will provide a reason for the rejection, ORDPLACE will log the reason provided by CRDCHK and set the status to 422, the OrderGateway will throw an exception, and the Order API will return a 422 error to the caller.
Dependency Management
Dependency management is the technique of identifying, controlling, and maintaining the components that an application relies on, such as third-party libraries, packages, and other internal or external components. It helps an application reuse components, resolve conflicts, and improve security.
Additionally, you can change the dependencies of an application as and when you need to, thereby enabling you to build loosely coupled applications. An application can comprise several dependencies, not limited to external components.
A typical IBM i environment may comprise several dependencies such as the following:
- Calling programs
- Physical and logical files
- SQL views and procedures
- Job descriptions
- User profiles
- Batch schedules
- External vendor systems
- File formats
- Message queues
The following code snippet shows how you can save the references for every program in ORDLIB to a file and then list the programs writing to ORDHDR.
DSPPGMREF PGM(ORDLIB/*ALL) OBJTYPE(*ALL) +
OUTPUT(*OUTFILE) OUTFILE(MYLIB/PGMREFS)
SELECT WHPNAM, WHFUSG FROM MYLIB.PGMREFS
WHERE WHFNAM = 'ORDHDR' AND BITAND(WHFUSG, 6) > 0;
The Importance of Observability
IBM i systems have a reputation for their reliability and dependability, but that doesn't negate the need for observability. Modern software solutions are expected to show a clear picture of what has happened within the application and to provide a detailed account of how the system is functioning, whether or not any issues occurred in production. Here is where observability helps. Observability helps you understand an application's internal state by analyzing its external outputs and data, and it comprises metrics, logs, and traces.
Observability provides answers to these practical questions:
- Which orders did not go through today?
- What is the API call timing to IBM i?
- Which batch job is running slow?
- Did deployment increase the database errors?
- Are failures coming from one component?
- Will it be possible to reproduce the path?
Correlation ID becomes critically important when a request passes between systems. Without a correlation ID, the support team would have to look through numerous logs by timestamp, job name, user profile, and order number. With it, you can track the transaction from start to finish.
The following code snippet shows how you can read the job log for an order.
SELECT L.EVENT, L.DETAIL, L.JOB_NAME,
J.MESSAGE_TIMESTAMP, J.MESSAGE_ID, J.MESSAGE_TEXT
FROM ORDLIB.ORDLOG L,
TABLE(QSYS2.JOBLOG_INFO(L.JOB_NAME)) J
WHERE L.REQUEST_ID = 'c599f0ff-c242-4ec6-8062-c24fc888b797'
AND J.MESSAGE_TIMESTAMP BETWEEN L.LOGGED_AT - 5 SECONDS
AND L.LOGGED_AT + 5 SECONDS;
In the preceding code snippet, the query finds the ORDLOG row for a particular request ID and passes the stored JOB_NAME to QSYS2.JOBLOG_INFO, which returns one row of log messages for each job.
Logging in RPG needs to be more than a mechanism for recording system activity; it needs purpose. Logging can be done in one of three ways: recording system activity in an Audit Table, transmitting messages, or using a centralized logging solution. What matters is that operation logs are consistent.
Management of Technical Debt is Crucial
Technical debt is often confused with "legacy code." Remember, though, legacy code isn't bad code. Technical debt isn't bad, either. Your legacy code may be years old, but if it's reliable, maintainable, tested, and cheap to maintain, it's always better than software that uses modern technologies and architectures but is crippled with bad design. To understand technical debt better, think of it as something you accrue because you took development shortcuts that add phases and increase future cost or risk.
Here are a few typical examples of technical debt:
- Business rules being duplicated
- Vague ownership of database tables
- Batch jobs that have dependencies that are unknown or not properly documented
- Programs with excessive responsibilities
- Unmanaged shared resources
- Lack of automated testing
- Hardcoded values of environment variables
- Manual deployment process
- APIs that lack versioning
The Modern IBM i Enterprise
For decades, the IBM i platform has remained a dependable, secure, transaction processing system. Upgrading an IBM i system doesn’t mean throwing away its capabilities. On the contrary, it enables you to utilize modern enterprise architecture methods, like layered architecture, service limits, dependency management, technical debt reduction, observability, documentation, and governance for application development that is powerful, implementable, and stable.
This blend can help you bridge the gap between legacy and modern applications to leverage the best of both worlds - you get the stability of your legacy IBM i applications with enhanced delivery velocity. Organizations can use this approach to leverage modern enterprise architecture, tools, and technologies while ensuring the business logic of their legacy RPG applications works as usual.
Why This Works: Enabling Legacy RPG Applications to Leverage Present-Day Technology and Architecture
For years, RPG developers have built legacy RPG applications using the strategies outlined in this article. If an RPG application has its parameters set up properly and performs a single task, it indicates the application has an isolated responsibility. If a service is being called by other applications instead of repeating the same logic, you can assume that the service boundaries have been identified and are being adhered to.
Layered architecture, service boundaries, dependency management, documentation, and governance in the above-mentioned issues provide a language to formulate existing concepts. Basically, these principles are the application of years of IBM i practice to APIs, .NET applications, and other systems that currently co-exist with RPG programs.
You also won't need to rewrite your core business logic to get those advantages; you just need to identify the service boundaries you already have. Instead, identify service boundaries and apply a consistent modernization approach in future projects.
Takeaways
- In an enterprise application, any component that owns a business rule should be the sole owner who can change the rule if need be
- You do not need to discard or change the business logic or your legacy RPG programs to take advantage of modern technologies and architectures
- Observability enables you to comprehend the internal state of an application by analyzing its external outputs and data
- Ensure that the dependencies you use in an application are taken from trusted sources only
- You should automate dependency management to ensure that your application can leverage the modified dependencies without any manual effort needed
Business users want new applications now. Market and regulatory pressures require faster application updates and delivery into production. Your IBM i developers may be approaching retirement, and you see no sure way to fill their positions with experienced developers. In addition, you may be caught between maintaining your existing applications and the uncertainty of moving to something new.
IT managers hoping to find new IBM i talent are discovering that the pool of experienced RPG programmers and operators or administrators with intimate knowledge of the operating system and the applications that run on it is small. This begs the question: How will you manage the platform that supports such a big part of your business? This guide offers strategies and software suggestions to help you plan IT staffing and resources and smooth the transition after your AS/400 talent retires. Read on to learn:
LATEST COMMENTS
MC Press Online