Overview

The “Service Stack” pattern is designed to enable high reusability in Node JS API projects that interact with a data store. This pattern is not intended to replace proper Node JS modules, but instead provide a clean approach to sharing business logic across services within a project.

Layers

The pattern consists of three clearly defined layers:

  • Service
  • Coordinator
  • Model

Service

The service layer will define the service end points or routes that will make up the API. The service class responsibilities include input validation, security, input parsing, response handling  and coordination with external APIs. The service layer is specifically designed to be built in a web framework such as Express JS, HapiJS, or KoaJS. It is designed to be used by any consumer of a rest API such as HTML5 applications, mobile apps, or other services.

Coordinator

The coordinator layer contains all business logic and is designed to be executed by the service and other libraries including stand-alone applications. This layer should be designed to operate like a library without the need to simulate components of a web based library.

Model

The model layer contains all data logic, data validation and interactions with the underlying data store. The model layer should be designed to shield the coordinator from the details of the underlying data store.

Basic Usage

In a basic scenario where the logic flows through a single service stack, the request is handled by the service calling one or more functions on the coordinator which proceeds to call one or more functions on the model.

Service-Coordinator-Model Pattern - Basic Flow

Referring to the diagram above:

  • A request is received by the service where the permissions are verified.
  • The input data is validated to check for required fields and malicious content.
  • The coordinator function(s) is called.
    • The service is required to parse the input data and pass it to the coordinator function(s) is the required format.
    • The coordinator will then execute any business logic and call the model.
    • In the instance of a CRUD application, there may be no business logic required so a simple pass through will be acceptable.
  • The model will take the input and perform data validation prior to making any modifications. Additionally some validation may occur during a data fetch request to ensure that valid search values have been provided.
  • Once the action is complete, the model is responsible for formatting the data and returning it to the coordinator where additional business logic may executed.
  • The service will process the results further and form a response.

Complex Usage

The basic scenario is fine for most CRUD applications, but advanced applications will have shared concerns. One service may need to access data maintained by another service to perform validation. In this instance, the components can then be used by the other service components.
In a complex usage scenario, the controller will call multiple coordinators in order to achieve the proper results. A coordinator may also call multiple models to achieve results. Being that this can occur, strong design of the coordinator and model functions should be performed to enhance the reusability of the layers.

Service-Coordinator-Model Pattern - Complex Flow

Coordinator/Model Design

When designing the coordinator and model components, there are some considerations that must be taken into account.

  • Does the service stack need to perform low latency tasks?
  • Is there stateful data that the service, coordinator and model all need to share?
  • Are the components of the service stack reusable?

Classes

Classes are useful when stateful data is to be shared between the service, coordinator and model layers. The coordinator and model should be implemented as classes and the stateful data should be passed in through the constructor or other injector function.

The ES6 specification and TypeScript both support using the “class” keyword. It is recommended that one or the other be used to make the code more readable. Note that TypeScript must be compiled to Javascript.

Note that when using classes, a new class hierarchy can be instantiated with each request and the garbage collector must clean this up after each call which may result in slower performance in a low latency application when the system is under load.

Singletons

Singletons are useful in low latency services because there is no need to instantiate a class hierarchy for each request. Having the coordinator and model layers implement singletons will eliminate the excess heap garbage that is generated by classes. Stateful information can be passed to the coordinator and model layers by adding additional parameters to functions.

Object Factories

Object factories may be used with classes to provide pooling of resources and reduce the amount of garbage collection that must be performed. Stateful data can be transferred to the class hierarchy by adding an injector function rather than using the constructor. Either have the factory function pass the data or the calling object can inject the data once the classes have been retrieved. New classes would only be created when the pool is empty and limits may be created for how many objects may be stored at any one time.

Design Considerations

Validation

JSON schema should be used to perform the validations used through out the pattern. By using JSON schema, the same validations present in the API can then be shared with 3rd party users such as other APIs or UIs. The tv4 library provides JSON schema validation up to the latest specification.

Promises

It is recommended that promise chains are used across all three layers to facilitate readability and code reuse. Promise provide a nice chain of calls that can be easily read while the callback method becomes harder to understand the deeper the call chain grows.
Popular libraries include Q and Bluebird as well as native promises.

Advertisements