Using a Service-Oriented Architecture Approach to VCF Automation Orchestrator Development

In this post, I will briefly describe Service-Oriented Architecture (SOA), and how I apply this to all of my VCF Automation Orchestrator development.

Service-Oriented Architecture (SOA) is a popular software development methodology, that focuses on creating distinct services that are loosely coupled and reusable. These characteristics make SOA an approach that is well-suited for systems integration. Anyone who works with Orchestrator will understand that most development focuses heavily on integrating with other systems, where Orchestrator acts as the “glue” or central point.

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 re-used 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.

Following the SOA principles helps to reduce the need to redevelop or duplicate existing functions (or Actions in the context of Orchestrator).

SOA lends itself well when developing integrations in Orchestrator that use either the Plugins or HTTP REST hosts.

Traditional Orchestrator Approach

The following is an example I often come across (and certainly something I have done in the past) of how code is developed in Orchestrator.

Let’s consider a hypothetical API ‘MyAPI’ that we want to integrate with, that 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 assume that the GET method supports query parameters, pagination (for collections) and additional headers. Now we have a situation where we have to update the 5 Actions to support these features. Pagination code alone could be 50 lines long, resulting in 250 lines of duplicated code.

When there are multiple integrations with dozens of endpoints, the situation can get ugly and become difficult to manage and scale.

An Approach based on SOA Principles

An alternative method would be to create a service that is used to interact with the ‘MyAPI’ API. We can use native features within JavaScript to achieve this, such as Classes and Prototypal inheritance. The service will be created as a single Action within Orchestrator.

Consider the following example Action called ‘MyApiService‘, which uses a class (function) declaration and required methods.

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;

ECMAScript5 used by VCF Automation Orchestrator does not support the ‘class‘ keyword and ‘function‘ is used for this instead.

A prototype provides a way to assign additional methods to our ‘class‘.

In this example, we have created methods to support all the required GET calls for the 5 endpoints and an additional method which handles the inner workings of the GET call itself (handling params, collections, pagination, etc).

To use this we use the ‘new‘ keyword to create a new object based on the MyApiService class, along with System.getModule();

var myApiService new (System.getModule("com.simplygeek.myapi").MyApiService())(restHost);
var result = myApiService.getEndpoint1;
...
var result = myApiService.getEndpoint3;

Not only does this look a lot cleaner, but it also provides the benefit where we are no longer duplicating code. It’s easy to add additional methods, and calls can share the same backend logic.

Now let’s consider another example, which extends from the previous example but this time uses Prototypal inheritance. This method allows us to break the ‘MyApiService‘ class into multiple classes, one of which will hold the backend logic and another for the “front end” calls.

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 have split the original MyApiService class into two classes, with the additional MyApiBackendService class. We have also extended the MyApiBackendService to include additional methods.

The magic that brings these two classes together:

MyApiService.prototype = Object.create(
    MyApiBackendService.prototype
);
MyApiService.prototype.constructor = MyApiService;

In modern languages, the keyword ‘extends‘ would achieve the same goal.

Using this concept, we can create a modular service that will hide complex logic from the front end. This also reduces the impact of making changes, as the backend service is less likely to be updated often.

If you have dozens or maybe even hundreds of calls, these could all be grouped into their respective services, that all share the same backend logic.

I hope this has been insightful and if you have any other techniques, then please share!