Having native constructs for expressing constraints on communication structures (such as “this can happen only after this” or “this is available until this happens”) makes it easy to understand what a service does. It also makes it easy to create a quick prototype out of a design idea for an MSA. Yet another important benefit is that the structure of communications can be changed compositionally. We’ll provide an example of this by changing our architecture in the following sections.
Change: Authentication as a service
Checking whether some credentials are valid for authentication is a task that often makes sense to delegate to a separate microservice, so you can reuse an existing database or support third-party authentication. We update our architecture accordingly:
Let’s assume this new authentication service, called Auth, supports distributed authentication (following the main ideas of OAuth and OpenID). Thus, when invoked on operation openAuth
by Posts, Auth replies with an authentication token that will be used to identify the authentication session. Auth then interacts directly with the client to check the user’s credentials (Posts does not see these interactions). Finally, Auth sends a message to Posts, either on operation ok
if the credentials were correct or on ko
if authentication failed.
Let’s update the code of Posts to put this into practice (changes are highlighted in bold text):
cset { authToken: ok.auth ko.auth }
main
{
auth(user)(redirect) {
openAuth@Auth(user)(csets.authToken);
redirect = AuthLocation
};
[ ok() ] {
provide
[ getPost(which)(post) { query@Database( ... )(post) } ]
[ newPost(post)(link) { /* update db ... */ } ]
[ editPost(edit)() { /* update db ... */ } ] [ deletePost(which)() { /* update db ... */ } ]
until
[ quit() ] // User logged out
[ timeout() ] // Session timeout
}
[ ko() ]
}
Above, we added the declaration of a correlation token authToken
for operations ok
and ko
. This means the operations can be invoked only by knowing the value of authToken
(in our case, the only external service knowing authToken
will be Auth). In the behavior, we update the implementation of operation auth
; instead of checking the user’s credentials, it opens an authentication session at Auth with operation openAuth
, retrieves the created authToken
, and redirects the client to the Auth service.
Now authenticating the user is the business of the Auth service. We simply wait to receive a notification from it telling us whether authentication was successful (ok
) or not (ko
). If ok
is called, we proceed as before. Otherwise, ko
is called and the process terminates immediately.
Addition: Comments as a service
We now add another service, called Comments, to handle a comment thread for each blog post. Comments provides operations for posting an anonymous comment (we omit authentication for simplicity in this example), called newComment
, and for retrieving all comments for a blog post, called getComments
. We update our design:
Here is the code for Comments:
inputPort Comments {
Location: CommentsLoc
Protocol: http
Interfaces: CommentsIface
}
main
{
[ newComment(comment)() { /* update db … */ } ]
[ getComments(whichPost)(comments) { /* read comments from db… */ } ]
}
The code is simple. Operations newComment
and getComments
can be invoked at any time by any client, and the request is handled by a parallel process each time. To deploy the new Comments service, we simply need to save the code in a file (say, comments.ol) and launch jolie comments.ol
. The service is now up.
Addition: Replicating Comments
Of course, in a lively website, there may be many more comments than posts. Luckily, with microservices, if we need more performance for handling comments, we merely need to replicate the Comments service. The key aspect here is that the rest of the system should not be impacted by this modification and will continue working as usual. We update our design yet again:
We can obtain the replicated Comments services by running the same Jolie code that we used for Comments, giving each replicated instance a different value for the location CommentsLoc
. Then we can deploy them on different machines (or cloud instances) without any problems simply by launching an instance of the Jolie runtime on each machine.
The interesting part here is the code for the Comments balancer service, which must hide all of this complexity away from the rest of the MSA. The code for the balancer:
outputPort Comments { Interfaces: CommentsIface }
inputPort CommentsBalancer {
Location: OriginalCommentsLoc
Protocol: http
Aggregates: Comments
}
courier CommentsBalancer {
[ CommentsIface(request)(response) ] {
// Choose a Comments service instance
Comments.location << /* what you chose */;
forward Comments(request)(response)
}
}
Above, we have an output port Comments
that we will use to communicate with the Comments service instance every time a client requests something. We expose the input port of the balancer on the same location that we used for our original Comments service so that the rest of the MSA remains unaware of the change.
Now, the key to our balancer is the Aggregates: Comments
declaration in the same input port. This means that every incoming message for the interface of a Comments service will not be handled by the balancer directly, but will instead be redirected to a service instance. The redirection code is in the courier
block that follows, which is triggered whenever a request for interface CommentsIface
is received. The code selects the Comments instance we want to contact, forwards the request to it, waits for the response, and finally sends the response back to the original invoker.
Aggregation, the method implemented by the keyword Aggregates
, is a native primitive for creating programmable proxies, enabling the management of cloud (or cloudlike) environments.
Addition: Implementing an API gateway
We are almost done. We now have a nice MSA that we like, but it is impractical. All clients must interact with multiple services to retrieve a blog post and its comments, generating many round trips. Also, different clients may have different needs depending on their form factor or supported formats. For example, we may want to differentiate how our APIs behave for desktop clients and mobile clients. This is exactly the problem addressed by the Netflix’s API gateway pattern.
In particular, we’ll update our design (for the last time, promise) to insert an API gateway between our clients and services:
Our gateway offers two subservices, called adapters, one optimized for desktop clients and another optimized for mobile clients. The code for the gateway is almost trivial. It is basically all deployment and no behavior:
inputPort Gateway {
Location: “socket://blogexample.com:80”
Protocol: http
Redirects: Desktop => DesktopAdapter, Mobile => MobileAdapter
}
embedded {
Jolie: “desktop.ol” in DesktopAdapter,
“mobile.ol” in MobileAdapter
}
Our gateway has an input port, Gateway
, that uses redirection with the Redirects
primitive. This means that through Gateway
, clients can access the APIs of two subservices, one called Desktop
and another called Mobile
, respectively pointing to our desktop and mobile adapters. (Bonus hint: Redirection is also great for versioning, as you can expose different versions of the same service.)
These adapters are actually embedded Jolie services, meaning that they are loaded together with the Gateway and automatically shut down when the Gateway is shut down. This is for convenience in maintaining the lifecycle of our services. If we need the adapters to be externally provided, we can erase the embedded code block. We can even embed services written in different programming languages if necessary (for now, Java and JavaScript are the supported options).
What does an adapter look like? For example, our desktop adapter offers a new operation called getPostWithComments
, which receives a single request and orchestrates the Posts and Comments microservices to form a complete response containing both post and comments that is then returned to the invoker:
main
{
[ getPostWithComments(whichPost)(response) {
getPost@Posts(whichPost)(response.post)
|
getComments@Comments(whichPost)(response.comments)
} ]
}
Observe that the invocations of Posts and Comments are composed with the operator |
, which means they will be performed in parallel.
A microservice programming paradigm
Jolie simplifies the development of microservice architectures by implementing typical microservice design methodologies through native primitives. This not only improves maintainability, but opens up a new world of quick prototyping to both newcomers and experts, who can check whether making one or other design choice gets them what they need in practice. Jolie replaces typically heavy approaches to service programming with a more agile approach focused on “hacking” microservice code.
For the Jolie open source initiative, determining which primitives connect the dots between design ideas and code means exploring a lot of uncharted territory. Luckily, we can “stand on the shoulders of giants,” ranging from theoretical models such as process calculi to many practical frameworks for concurrent programming (such as WSBPEL and Erlang). We try to push on by maintaining a collaboration network among universities and contributing companies.
The microservice paradigm and community are evolving rapidly, and many new tools are appearing at a very fast pace. We believe in a holistic approach. Both the Jolie language and runtime are extendable to integrate with other service technologies and container frameworks. For example, we at ItalianaSoftware regularly use Jolie to convert SAP-backed enterprise software to microservice architectures.
All Jolie services come with a pluggable monitoring architecture in which the monitor of a service is itself a microservice. This means that third parties can plug in their preferred tools. The same goes for application containment: Users may choose the monitoring and cloud frameworks offered by ItalianaSoftware or provide their own. We are currently looking into building plug-ins for Docker and some of the microservice tools open sourced by Netflix.
Many thanks to Saverio Giallorenzo and Claudio Guidi for their useful comments and suggestions.
Fabrizio Montesi is Assistant Professor at the University of Southern Denmark and a Founder Director of ItalianaSoftware. He has authored many scientific publications in the field of programming languages and service-oriented computing. He is co-creator of the Jolie programming language and leads the project, together with Claudio Guidi.
New Tech Forum provides a venue to explore and discuss emerging enterprise technology in unprecedented depth and breadth. The selection is subjective, based on our pick of the technologies we believe to be important and of greatest interest to InfoWorld readers. InfoWorld does not accept marketing collateral for publication and reserves the right to edit all contributed content. Send all inquiries to newtechforum@infoworld.com.