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:
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:
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.