by Fabrizio Montesi

Hack your way through the microservices revolution

analysis
Mar 31, 201519 mins

How can we go from the design of a microservices architecture to code as fast as possible? With a programming language created for the purpose

Microservice architectures (MSAs) are emerging as a paradigm for the design and development of scalable software. In an MSA, a software component can be not only a typical library, but also an independently running service that interacts with the other parts through message passing. The independence of services helps with achieving high cohesion and loose coupling, which deliver all sorts of benefits (reliability, scalability, reusability, and so on). But the real deal maker is another interesting property: Services can be redeployed with different replication and location configurations as the design of the overall system evolves.

Even the technology used for the implementation of a service can be changed, as long as its interface is still compatible with the rest of the system. MSAs are therefore very flexible architectures. For example, the fact that each single-service component can be replicated in isolation, without altering the behavior of the other parts, allows developers to introduce scalability where it is needed instead of replicating the entire architecture, saving on resources. A precise definition of microservices is still the object of discussion, and it will be for a while, as the paradigm is still being built from the ground up among practitioners. Martin Fowler’s March 2014 article on microservices offers a first overview of its inception, and it’s an excellent first read for anybody approaching the topic.

The reader familiar with service-oriented architecture (SOA) might well wonder, “What is the difference between SOA and MSA?” While both SOA and MSA are centered on the idea of a service, the main difference lies in the scale at which this idea is applied: A service in an SOA is usually an entire software application, whereas a service in an MSA is a software component. (In an MSA, applications are again services, but obtained by composing service components.) MSAs are thus much more granular than an SOA.

On the design level, MSAs look like a good candidate for mastering the typical “systems of systems” that are pervasive in enterprise software and in new initiatives such as the Internet of things. On the practical level, this shift toward using services as components comes with a requirement of being more lightweight than SOAs. Indeed, developers of microservices tend to use what works best for their scenario, rather than sticking to one size fits all (or nobody?) standards like SOAP or XML. While SOAs and MSAs are based on the same underlying principles, the sheer granularity of MSAs brings new possibilities and problems to the table.

Architecture matters

Microservices may sound like the right thing for your next project. In theory, they offer you a new level of flexibility and scalability. In practice, however, going from the design to the prototyping to the final implementation of an MSA can be a daunting task.

MSAs shift the boundaries of message passing from the outside of your software to the inside: service components in an MSA work by exchanging messages with each other. To build an MSA, developers need to be able to build independently running services, program their coordination via message passing, and manage their deployment. Handling all of these aspects is difficult already in a typical SOA. At the level of granularity of an MSA, they can make development painstakingly slow for newcomers and even for expert programmers.

It can be tempting to deal with these issues by assigning the responsibility for each microservice you have to develop to a separate team. However, you should not do this unprepared. In some cases, it can even lead to inefficient teamwork and make the team lose sight of the overall architecture. You can have a beautifully compartmentalized MSA, but if your services interact poorly, you will still get abysmal performance. In fact, one of the first recognized examples of a microservice pattern, Netflix’s API gateway, focuses precisely on this point: optimizing communications between an MSA and remote clients.

Summarizing, two important lessons about microservices are merging. First, the architecture is what matters. Focus on the overall picture of how services are distributed and interact before optimizing their internal implementations. Using third-party services is fine as long as there are clear interfaces and developers are aware of how they affect the rest of the MSA.

Second, do not take the flexibility of MSAs for granted. You have to work for it. Remember to optimize for speed of development. Avoid setting architectural designs in stone.

If we consider these two points, it follows that a development process for MSAs should account for frequent changes in service deployment and interactions. Arguably, the game changer brought by MSAs is that they allow for experimenting with altering the overall service architecture, such as by replicating a service component or by changing a communication pattern among some services. Fine-grained tinkering with the service architecture of a system is what microservices are all about and where all their power comes from.

Thus, we need effective tools to quickly experiment with different microservice architectures. Granted, who doesn’t want effective tools? But MSAs are a very special case. If we do not make it easy to tinker with different architectures, newcomers will continue trying to solve problems by changing the internal details of microservices, simply because changing the architecture is too time consuming. We need to flip this over, as others already understood. This is why Netflix developed so many tools and why platforms such as Docker are already so popular for microservices.

A native language for microservices

At ItalianaSoftware, we do microservices for a living. As a technology provider, we have gained experience both with our own teams and with those of other companies that use our expertise and tools to develop and operate microservices.

In our time with microservices, we have observed an interesting (and justified) psychological barrier. Whether experts or beginners, many developers approaching microservices for the first time see a “service” as something that is inherently complicated and requires a lot of setup. They are accustomed to using a specific tool for each service — or type of service, such as a Web server or an orchestrator — with the consequent burden of spreading the configuration of an MSA over multiple configuration files, possibly written in different languages.

If developers view the initial setup of an MSA as so difficult, it is not surprising that they are also daunted by the prospect of maintaining microservice architectures. How could we convey the idea that, on the contrary, the power of software solutions based on microservices comes from their flexibility and changeableness? This is where Jolie comes into play.

Jolie is a programming language. It was created with the intention of making the programming of services easier. Back when we created Jolie, we were doing SOAs differently based on recent results in the field of concurrency theory for message passing. We wanted to empower developers to easily experiment with different service architectures and to make service programming more accessible to newcomers. We figured that if we could make it easy for newcomers, we could also make experts much more productive. Years later, this approach has become a methodology for building MSAs.

Jolie is a language tailored for the application of microservices, just as Java is tailored for the application of object-oriented programming. The goal behind Jolie is to make a programming paradigm out of microservices and to embody this paradigm in the language. Drawing an analogy, we strive to make programming services as easy as programming procedural code became after the appearance of language featuring procedures as native constructs. Jolie is, to the best of our knowledge, the first attempt at defining a native programming language for microservices.

In addition to the programming of communication patterns, or conversations, among microservices, Jolie handles the programming of service interfaces (and their composition) and deployment strategies. It simplifies service programming by tying together these three main aspects of microservices while using only minimal contact surface among them to achieve code reusability.

Example: Programming a blog service in Jolie

We now go all the way from the philosophy behind Jolie to using the language. In the following example, we’ll get our hands dirty with a microservice architecture for a blog service. (I’ll omit code that is not relevant for our discussion, such as database queries.) We will end up having an architecture that looks like this:

Jolie blog architecture

But that will be the end result. We will start simple with a basic service. As we go, we will change our architecture to add authentication and comments services, replication, and an API gateway. Architectural prototyping is extremely important to us. If a developer comes up with an idea for restructuring an MSA, she should be able to try it out without fear of ending up with unmaintainable code. Jolie is optimized for speed by design.

A Jolie program implements a microservice, which can be deployed simply by running the Jolie interpreter in the desired machine or cloud instance. A program is composed of two parts: deployment and behavior. In the deployment part, the programmer specifies the interfaces and connections with the rest of the system. In the behavioral part, the programmer defines the actual implementation of the service — that is, the communications and computations it performs and in what order. Separating deployment from behavior allows developers to have a clear view of the connections between a microservice and the rest of the MSA, without having to look at its concrete behavior. It also allows developers to change the deployment of a service independently of its implementation.

Let us do design first, then code. We want a simple Posts microservice for the administration of (you guessed it) blog posts. It should provide operations for manipulating and reading blog posts published via HTTP. These operations should be accessible only after a user is successfully authenticated and should be no longer accessible to the user after she logs out. Thus, our architecture looks like this:

Jolie clients posts

We start with the deployment of our microservice:

interface PostsIface { RequestResponse: getPost(string)(Post), … OneWay: quit(QuitMsg), timeout(TimeoutMsg) }

inputPort Posts { Location: PostsLoc Protocol: http Interfaces: PostsIface }

The deployment part of the Posts microservice starts by defining its interface. Service interfaces in Jolie are composed of operations and their types, similar to Web services. RequestResponse operations are expected to reply to the invoker, whereas OneWay operations are not (they are one direction only, as the name implies). The input port Posts tells Jolie how to receive invocations for this microservice. It deploys the service interface on the IP address PostsLoc (such as localhost:8080), which is parameterized and given by the environment, with HTTP on top as transport. Nevertheless, Jolie can use different communication technologies and data formats, without requiring changes to the behavior of the service.

Let us proceed now with the behavior of the Posts microservice, which defines the implementation of our interface:

main {      auth(creds)() { if ( /* creds not good */ ) throw(AuthFailed) };      provide         [ getPost(which)(post) { query@Database( ... )(post) } ]         [ newPost(post)(link) { update@Database( ... )() } ]         [ editPost(edit)() { update@Database( ... )() } ]         [ deletePost(which)() { update@Database( ... )() } ]      until         [ quit() ]         [ timeout() ] }

The main procedure (the entry point of a Jolie service) contains the behavior of the service. It is a structured process based on communications. In our code, whenever a client invokes the operation auth, a dedicated parallel process to handle the session with the client is started. The auth function is a RequestResponse operation: It receives credentials (creds) and checks whether they are good; if they are not, it raises a fault that is automatically sent back to the invoker and terminates the rest of the process. If auth goes well instead, we enter the provide until block. We can read this as “the operations getPost, newPost, editPost, and deletePost can be invoked as many times as needed, until either the quit or timeout operation is called.”

Each operation in the provide until block has its own implementation, which provides the proper interaction with a database for creating, editing, deleting, and viewing a blog post. In Jolie, a database is also a microservice (actually, everything we can interact with is a microservice), so the latter is obtained through the invocation primitive.

As an example, take this line:

[ getPost(which)(post) { query@Database( … )(post) } ]

We can read it as “when getPost is invoked, store the received message in variable which, call the Database service on operation query, wait for a response to store in variable post, and finally reply to the initial invoker of getPost with the content of variable post.”

Starting our Posts service is easy. We simply save the program in a file (say, posts.ol), launch jolie posts.ol from a shell, and the service will be online.

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:

Jolie clients posts auth

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:

Jolie clients posts comments

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:

Jolie clients posts comments services

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:

Jolie API gateway

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.