Pega Patterns - Services
Pega has long defined a pattern for handling the connection of a PRPC system to an external system via what we in the business affectionately call the “Int Layer”. It says that you shouldn’t tightly couple your application’s data model to that of any external systems, but should instead define two separate class hierarchies:
The idea here is that your application serves as the controller between its own data layer and those of the external systems with which it interacts. You define a few rules in your application that handle the mapping into and out of the external system’s Int Layer representation, and the rest of your application runs on its own data model, free of the worries of swapping out integrations (or systems of record) down the road. Your application is the puppeteer, the integrations are the puppets.
But what of Services that your Pega system exposes?
Building and using the Int Layer pattern that Pega provides for calling systems outside of Pega forms two layers in a dependency layer-cake:
Your application’s code can see down into the Integration Layer, but not vice versa. This makes sense in the puppeteer analogy since your application will control the integrations, and the integrations will serve the needs of your application (and any other consumers).
Services, on the other hand, are really the direct inverse of this pattern: they are the controllers of your application acting on behalf of some external system that is really calling the shots. This updates our dependency layer-cake:
Pega sometimes recommends the use of MyCo-Int- derived classes here, but this leads to confusion in practice. You end up sandwiching your application’s code between two seemingly identical layers of classes with no clear indication as to which ones can control your code and which ones your code controls.
While it’s true that a Services layer is an Integration layer of sorts, it’s a very specialized one that is not beholden to the rules and requirements (and naming conventions and patterns) of external systems that define their own contracts. You own the contract here. Therefore, drawing a clean line between each of these three layers of the application will bring greater clarity to your code and what belongs to each layer. Just like the structure sits Services on top of your application which, in turn, sit above the Integrations, a companion class structure must also exist to clearly isolate concerns:
More often than not, your Enterprise layer’s Service root class (MyCo-Svc-) will provide foundation elements (much like the MyCo-Int- class) but not really provide any particular functionality. The functionality will be defined in classes that live in either the Framework Layer or the Implementation Layer of your application.
Framework or Implementation
As a best practice, if any Implementation may need to expose its functionality via services (or a full-fledged API), the service layer should really be defined at the Framework Layer. This makes the ability to expose that same functionality available to other implementers and keeps the contract consistent with just an endpoint change and maybe a few extensions.
The Implementation Layer should only define the initial Service Layer for a given application when the underlying Framework is NEVER intended to be exposed to external consumers. This would typically only occur when talking about private APIs or components that are defined inside of the PRPC world and intended for use by other, local, PRPC applications.
“But if the services are defined in the Framework, how will they create MY work objects and MY data objects in MY Implementation Layer,” you ask? Well, that’s where we get back into the Dynamic Class Referencing Pattern. Your Service code is layered on top of the code it orchestrates. Therefore, it has visibility into that code and (necessarily) the ability to execute and reference it: you have the ability to instantiate pages using the same DCR library that your application uses when running natively.
When you expose a service to the outside world (it doesn’t matter if it’s through PRPC or any other technology), you also define a contract. This contract is the well-defined language through which external systems communicate with your application. It’s all too tempting to have your service layer simply pass data through to your application layer because, well, it sits on top of your application’s code and has direct line-of-sight down into it. Don’t fall into this trap, though. It’s certainly easy, but it also creates a tight coupling between the contract you publish to external systems and your application’s internal structure. Your application should be flexible and readily changeable while your service contract is rigid.
Instead, the pattern used in the Integration Layer should again be used for services: Receive Request – Map – Orchestrate – Map – Return Response. While this is more work when it comes to defining the service itself, it serves as protection to both your consumers and your application developers.
Your consumers should be able to rely on the contract you define without concern for it being built on shaky ground. The contract is rigid and should only change when there are no other means by which to accomplish a given goal.
You application developers should have the latitude to add, change and remove functionality as the requirements of the business morph over time. Worrying about whether a change to the application’s data structure will be backwards incompatible with exposed services artificially limits what they can do to grow the app.
So what does this look like in practice? Your contract should be defined through a set of Service Data classes descending from MyCo-Svc-Data- (or more appropriately, MyCo-FW-MyApp-Svc-Data-). External requests speak through this hierarchy exclusively (whether through XML, JSON, or some other data format is inconsequential). Data Transforms in your Service Layer then handle the work of translating from those Service Data classes and values into your application’s native classes and values. The Service Layer then executes the orchestration-related calls against those newly created pages. The opposite mapping happens at the end of processing to generate the response.