Using a Service-Oriented Architecture Approach to VCF Operations Orchestrator Development
In this post, I will provide a brief overview of Service-Oriented Architecture (SOA) and explain how I apply it to all my VCF Operations Orchestrator development.
Service-Oriented Architecture (SOA) is a widely adopted software development approach that emphasises the creation of loosely coupled, reusable services. These principles make SOA particularly well-suited for systems integration. For those familiar with Orchestrator, this alignment is clear; most development efforts centre around integrating with external systems, positioning Orchestrator as the “glue” or central coordination point within the automation ecosystem.
Here are some key principles of SOA:
- Modularity – Integrations are divided into smaller, self-contained services. A single service can also be broken down into smaller sub-services.
- Loose Coupling – Each Service is (mostly) independent, which helps to minimise dependencies. However, services can be composed of other services.
- Reusability – Services can be reused in other Services, Workflows or Actions.
- Scalability – New Services can be added easily.
- Mask Complexity – The inner workings of the Service can be hidden or abstracted.
Adhering to SOA principles helps minimise the need to redevelop or duplicate existing functionality, particularly Actions in the context of Orchestrator, by promoting reuse and modular design.
SOA is particularly well-suited for developing integrations in Orchestrator, whether you’re working with built-in plugins or external systems via HTTP REST hosts.
Traditional Orchestrator Approach
The following is a common example I frequently encounter, and admittedly have done myself in the past, of how code is typically developed in Orchestrator.
Let’s consider a hypothetical API ‘MyAPI’ that we want to integrate with, which exposes 5 endpoints. We’re not worried about the complexities of making such calls, but just the high-level idea. We will call these endpoints ‘MyAPI/endpoint1’ through to ‘MyAPI/endpoint5’, all supporting the method GET.
In Orchestrator, what I will often see developers create are 5 Actions (functions)
getEndpoint1
getEndpoint2
…
getEndpoint5
This is because you are almost encouraged to write multiple Actions, and many of the built-in Actions provided are also structured in this way.
You would call each of these Actions using System.getModule().
var result = System.getModule("com.simplygeek.myapi").getEndpoint1(restHost); ... var result = System.getModule("com.simplygeek.myapi").getEndpoint5(restHost);
But this appears to follow the SOA principles I mentioned earlier, right? Well, sort of…
Let’s consider a hypothetical API called MyAPI, which exposes five endpoints. For this example, we’ll focus on the high-level concept rather than the technical details of making the calls. The endpoints are named MyAPI/endpoint1
through MyAPI/endpoint5
, and each supports the HTTP GET
method.
When dealing with multiple integrations involving dozens of endpoints, things can quickly become messy, making the solution difficult to manage, maintain, and scale effectively.
An Approach Based on SOA Principles
A more scalable approach is to create a dedicated service for interacting with the MyAPI API. This can be accomplished using native JavaScript features such as classes and prototypal inheritance. The service itself can be implemented as a single Action within Orchestrator, encapsulating all logic related to the API in a clean, reusable structure.
Consider the following example Action named MyApiService
, which defines a service using a class-style function declaration and includes the necessary methods to interact with the API.
function MyApiService(restHost) { this.restHost = restHost; this.baseUri = "https://myapi.local/api/"; } MyApiService.prototype.getEndpoint1 = function () { // Perform a GET on endpoint1 var uri = this.baseUri + "/endpoint1"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint2 = function () { // Perform a GET on endpoint2 var uri = this.baseUri + "/endpoint2"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint3 = function () { // Perform a GET on endpoint3 var uri = this.baseUri + "/endpoint3"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint4 = function () { // Perform a GET on endpoint4 var uri = this.baseUri + "/endpoint14"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint5 = function () { // Perform a GET on endpoint5 var uri = this.baseUri + "/endpoint5"; var results = this.get(uri); return results; }; MyApiService.prototype.get = function () { // Backend code to handle GET call (handle params, pagination, etc) }; return MyApiService;
Since VCF Operations Orchestrator uses ECMAScript 5, the class
keyword is not supported. Instead, constructor functions are used to define classes, following the classical function-based approach.
The prototype allows us to define and attach additional methods to our “class,” enabling more efficient memory usage and consistent behaviour across all instances.
In this example, we’ve defined methods to handle each of the five required GET
endpoints, along with an additional internal method that manages the core logic of the GET
request, such as handling parameters, collections, pagination, and other common concerns.
To use the service, we create a new instance of the MyApiService
class using the new
keyword. This is typically done alongside a System.getModule()
call to reference the Action where the class is defined.
var myApiService new (System.getModule("com.simplygeek.myapi").MyApiService())(restHost); var result = myApiService.getEndpoint1; ... var result = myApiService.getEndpoint5;
This approach not only results in cleaner, more readable code but also eliminates duplication. It makes it easy to add new methods as needed, while allowing multiple API calls to share the same underlying logic, improving maintainability and consistency.
Now let’s take the previous example a step further by introducing prototypal inheritance. This approach allows us to split the MyApiService class into multiple, more focused classes, one responsible for the core backend logic, and another for the higher-level “frontend” API calls. This separation enhances modularity and promotes better code organisation.
Example Action: MyApiBackendService
function MyApiBackendService(restHost) { this.baseUri = "https://myapi.local/api/"; this.mediaType = "application/json"; this.restHost = restHost; } MyApiBackendService.prototype.get = function () { // Backend code to handle GET call (handle params, pagination, etc) }; MyApiBackendService.prototype.post = function () { // Backend code to handle POST call }; MyApiBackendService.prototype.delete = function () { // Backend code to handle DELETE call }; return MyApiBackendService;
Example Action: MyApiService
function MyApiService(restHost) { // Import Properties defined on MyApiBackendService. MyApiBackendService.call(this, restHost); } // MyApiService will inherit methods from MyApiBackendService. var MyApiBackendService = System.getModule( "com.simplygeek.myapi" ).MyApiBackendService(); MyApiService.prototype = Object.create( MyApiBackendService.prototype ); MyApiService.prototype.constructor = MyApiService; // Add additional methods to MyApiService MyApiService.prototype.getEndpoint1 = function () { // Perform a GET on endpoint1 var uri = this.baseUri + "/endpoint1"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint2 = function () { // Perform a GET on endpoint2 var uri = this.baseUri + "/endpoint2"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint3 = function () { // Perform a GET on endpoint3 var uri = this.baseUri + "/endpoint3"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint4 = function () { // Perform a GET on endpoint4 var uri = this.baseUri + "/endpoint14"; var results = this.get(uri); return results; }; MyApiService.prototype.getEndpoint5 = function () { // Perform a GET on endpoint5 var uri = this.baseUri + "/endpoint5"; var results = this.get(uri); return results; }; return MyApiService;
In these examples, we’ve refactored the original MyApiService
class by introducing a separate MyApiBackendService
class. We then extended MyApiBackendService
to include additional methods, allowing MyApiService
to focus on higher-level API interactions while reusing shared backend functionality.
The key that brings these two classes together lies in the inheritance mechanism, enabling MyApiService
to seamlessly build upon the foundation provided by MyApiBackendService
.
MyApiService.prototype = Object.create( MyApiBackendService.prototype ); MyApiService.prototype.constructor = MyApiService;
In modern programming languages, this type of inheritance is typically achieved using the extends
keyword. By applying the same concept in ECMAScript 5 through prototypal inheritance, we can build modular services that encapsulate complex backend logic and expose only the necessary functionality through a clean, front-end interface.
This separation not only improves maintainability but also reduces the impact of future changes, as the backend logic remains stable and reusable. When dealing with dozens, or even hundreds, of API calls, they can be organised into distinct service modules that all share a common backend foundation.
I hope you’ve found this overview insightful. If you’ve developed your own techniques or have alternative approaches, I’d love to hear about them. Feel free to share!
And this can be extended to Polyglot modules as well.