1 Introduction

Microservices is an emerging paradigm where that components (even the internal ones) are autonomous and reusable services [4]. Applications are built by composing services as black boxes, using message passing.

The nature of microservices fosters granularity, and a MicroService Architecture (MSA) typically consists of many individual services. Since services are independent and their coordination happens only through message exchanges, code reuse (the focus of this work) takes a different form than that found in standard approaches based on software packages. Typically, in other paradigms, packages are software libraries, i.e., pieces of source or compiled code that become a part of the execution of the main application (e.g., through source inclusion, or static/dynamic linking). While this approach can be used for developing an “atomic” microservice (a service that does not contain other services), it falls short of capturing the essence of the paradigm and how it is used.

There are two key aspects that we need to keep in mind when dealing with code reuse in microservices. First, a common pattern in service development is to resolve the dependency of a service simply by binding it to an externally-provided service (available somewhere else in the network), instead of importing code to be run locally. Second, if we do decide to import some code to be run locally, that code should still be run as a separate and independent “local” service. This way, if we need to change strategy later on (say, when we go from development to production) and switch from running a dependency locally to binding our service to an external provider, we can do it without changing our implementation.

Package managers for mainstream technologies were not built with MSAs in mind, so these two patterns are not natively supported. Microservice developers must instead typically resort to ad-hoc conventions to deal with these problems.

In this paper, we report on the development of a package management system for the Jolie programming language [5]Footnote 1: the Jolie Package Manager (JPM). Jolie supports microservices natively, so it is a prime case study for the development of a package system for microservices that deals with the aforementioned aspects. We illustrate how JPM supports the configuration and use of service packages. Furthermore, JPM supports a notion of interface parametricity (polymorphism), which can be used to develop services whose behaviour is determined by the interfaces of the other services that they are bound to at deployment time. Parametricity is necessary because these interfaces are known only when packages are “linked” to each other (to solve dependencies).

2 A Simple Example

We briefly introduce Jolie with a small e-shop example.

figure a

Listing 1.1 shows a simple \(\small {\texttt {Shop}}\) service. It has a single \(\small {\texttt {checkout}}\) operation, defined in Lines 11–13. The \(\small {\texttt {Shop}}\) has two dependencies: the \(\small {\texttt {PaymentProcessor}}\) and the \(\small {\texttt {Warehouse}}\), given as output ports. An output port dictactes how we can invoke another service. The output port \(\small {\texttt {PaymentProcessor}}\) is defined in Lines 5–8. This includes a attribute, which defines where the service can be contacted, a attribute, which defines the transport protocol to be used, and a list of statically defined , which types the API of the service.

The current practice to make Jolie services configurable is based on ad-hoc conventions. For example, we may include a file named \(\small {\texttt {config.iol}}\) that contains some constant definitions (representing configuration parameters). Listing 1.2 shows the configuration file for the \(\small {\texttt {PaymentProcessor}}\).

figure b

Here we provide a few fields for the behaviour of the service (\(\small {\texttt {TEST\_MODE}}\) and \(\small {\texttt {ACCOUNT\_MODE}}\)) and configuration for the input port of the service (\(\small {\texttt {LOC}}\) and \(\small {\texttt {PROTOCOL}}\)). The main problem of this approach is that this file needs to be included as source code by the service that we are configuring. This hides what the parameters mean (Are they bindings or not? What is the resulting architecture?). It also opens to security risks: since we are importing source code, an attacker may insert arbitrary malicious code in the configuration file and it would be executed.

3 Packages and the Package Manager

We introduce a package abstraction to the Jolie language and provide a tool that combines packages with configurations to achieve our aims from the Introduction, the Jolie Package Manager (JPM). A package is a folder containing Jolie source code. The code of a package is read-only when used as a dependency, to enable potential updates and integrity checks when packages are installed.

JPM distributes packages following a relatively standard approach. A package in a repository is equipped with a package manifest. A manifest contains information about the package, used for indexing (e.g., name, description, purpose, etc.) and package management (e.g., dependencies and version). We omit the details of manifests and how they are used to install packages from repositories, since these are similar to those in mainstream package managers. In the remainder, we focus on features that are peculiar to JPM.

3.1 Configuration

Jolie packages are configured by configuration profiles, which we introduce here. Crucially, profiles do not need to be included as source code by packages. They are instead given in separate deployment files (written by the user of the package) that are processed by the Jolie toolchain in a controlled way, when we need to run the services given inside of the package. The syntax of profiles recalls that of the Jolie constructs that can be configured. In Listing 1.3, we show an example of a configuration profile for the \(\small {\texttt {PaymentProcessor}}\), which replaces the ad-hoc source-included configuration file given in Listing 1.2.

figure c

This snippet shows a single profile named . A profile provides binding information (location, protocol) for communication ports and configuration values to a particular package. A user can provide different configuration profiles, e.g., one for development and one for production, and select among them at deployment time.

We require that the configurable elements of a Jolie program are marked with a new keyword, . This allows the developer to omit binding information in communication ports; the omitted field need then to be provided externally by a configuration profile. We do not allow setting the part of a port externally, since this would prevent type checking of programs until they know their deployment setup (typing how they use ports inside of their behaviours). Thus in the \(\small {\texttt {PaymentProcessor}}\) its input port is defined as:

.

3.2 Embedded Dependencies

In Jolie, all components are services that run independently. Sometimes, for performance or convenience, it is useful to embed a service in the same local VM (Jolie is implemented in Java). Services in the same VM still exchange messages, but they can use efficient in-memory channels, as opposed to the network.

Our new package system allows us to embed pre-configured Jolie packages in two ways. These two ways give a system administrator the freedom to choose and apply the best deployment strategy without any changes required to the services. We start by looking at the externally configurable approach.

Figure 1 shows a development configuration for the \(\small {\texttt {Shop}}\). In this configuration, we embed the \(\small {\texttt {PaymentProcessor}}\) and its dependencies inside the \(\small {\texttt {Shop}}\). Listing 1.4 depicts the desired deployment. When we state that a service should be embedded, we simply pass the name of the configuration profile to be used.

Fig. 1.
figure 1

Desired deployment configuration for our development build. The dashed region represents services inside of the same VM.

figure d

4 Parametric Interfaces

Proxy services delegate the computation of replies for their requests to other services. A notable example is circuit breaker [8]. We summarise this pattern in the following (see [7] for a thorough discussion in Jolie).

Circuit breakers attempt to protect against some of the problems that occur when using remote calls, such as connection problems, timeouts, and critical faults. During normal operation, a \(\small {\texttt {CircuitBreaker}}\) functions like a normal proxy between a \(\small {\texttt {Client}}\) and a \(\small {\texttt {TargetService}}\). Monitoring code inside of the \(\small {\texttt {CircuitBreaker}}\) attempts to detect problems. If enough problems are detected, the \(\small {\texttt {CircuitBreaker}}\) will start failing immediately without attempting to proxy the call. After a period of time it will start allowing some calls through, and eventually transition back to the normal state and allow all calls through.

Packaging proxy services like circuit breakers raises a particular problem. Intuitively, the proxy should only accept calls for operations that are declared in the interface of the target service. However, this interface is known only at deployment time, since we may want to reuse the same circuit breaker package to protect different services. Proxy services are thus inherently parametric on the interfaces of the target services that we choose at deployment time. To address this problem, we introduce the notion of configurable interface to Jolie.

figure e

In Listing 1.5 we define an externally configurable interface. A concrete interface is bound to it at deployment time by reading the configuration, giving the service, in this case \(\small {\texttt {CircuitBreaker}}\), the information needed to correctly proxy operations. Observe that since we want Jolie services to be type-checkable without knowing their configuration (since configuration may change in different deployment setups), this means that the behaviour of the service is necessarily defined as polymorphic, i.e., it cannot assume any specific operation in the configurable interfaces (this is obtained through aggregation in Jolie, see [5]).

To create a circuit breaker running in a client, we would embed the circuit breaker locally and have it bound to our external payment processor, this is shown in Listing 1.6.

figure f

We can just as easily create a circuit breaker that operates server-side (intercepting incoming calls), or as a proxy in the network, by adopting different deployment files for .

5 Related Work

A common approach to simulate bindings to external services in mainstream languages is to communicate with services via stub libraries. A stub library provides an interface that resembles that of the target service, and internally delegates all work to the latter. In web architectures, these libraries can be synthesised from specifications, for example using OpenAPI [1] specifications in tools like Swagger [9]. Our approach applies directly to web development through Jolie [6].

Seneca [2] is a microservice toolkit for Node.js, where business logic is enclosed in a plugin typically distributed as a Node.js module. By including these plugins as dependencies, a server can essentially embed the logic of these plugins, similarly to JPM. Seneca uses pattern matching to determine which service should handle a particular message. This makes the bindings of a Seneca service implicit, in contrast to JPM, where bindings are explicit. A similar mechanism may nevertheless be implemented in Jolie and JPM by adopting proxy services, cf. [3].