Thanks to Martin's great help, Jolie 1.4 will have support for Internal Services and enhanced local locations. If you feel like testing, you can already find it in our git repository (see compile from sources).
While these features do not add any essential expressiveness to Jolie, they make it easier to program non-distributed microservice architectures (NMSA here for brevity). "Wait" - I hear you - "NMSAs?"


Non-distributed Microservice Architectures (NMSAs)

When I teach somebody (students or professionals) how to program microservices, I pay attention to following a very gradual path. The fact is, there are simply too many things to worry about when you program Microservice Architectures (MSAs). If you are not an expert, you are going to get overwhelmed by the complexity. Even if you are an expert, you may be better off with the MonolithFirst approach.

But why are Microservices so complicated? The main reason is that an MSA is typically distributed, and distributed computing is hard (see the fallacies of distributed computing). So, intuitively, it is much simpler to start with a monolith, and switch to microservices later. Unfortunately intuition is not really met by reality here. Developing a monolith does not force programmers to make their components modular and loosely-coupled enough to be smoothly ported to microservices; for example, it is way too tempting to share resources and/or develop APIs that are not suitable for message passing. Most probably, your initial monolith will end up being a SacrificialArchitecture. This is not necessarily a bad thing: many successes were built on top of sacrificing architectures. I am more worried about the fact that this is not really a smooth learning process: develop a monolith, learn something, now change everything.

NMSAs is a style where you program your system as if it were an MSA, but without the distribution. In an NMSA, Microservices are run concurrently in the same container (in our case, a Jolie-controlled JVM). It is an attempt at providing an easier setting for learners and experimenters to start with: no distributed computing means no fallacies of distributed computing. What can you learn in such an environment? A great deal, including:

  • You can learn how to develop loosely-coupled service interfaces, and how to deal with message passing in the easier setting of a perfectly-working "network".
  • You can learn how to test your services, and how to automatise testing.
  • You can learn how to monitor service execution.
The idea is to first approach the programming of microservices in this simpler ("fake", if you like) setting, and learn how to deal with distribution later. In the Jolie programming language, this path from NMSAs to MSAs is supported by the language itself.

NMSAs first...

NMSAs are created in Jolie by using embedded or internal services, i.e., services that run locally in the same container of the main service program. I will refer to such services as "local services" here. All components that you can write in Jolie are services, therefore all the architectures that you write in Jolie are service-oriented by design. This means that components cannot share state, and must interact through service interfaces based on message passing. What happens behind the scenes depends on the deployment information that you specify for your services, which is kept separate from the application logic. If the case of a local service, messages are efficiently communicated via shared memory, but without breaking the message passing abstraction given to the programmer.

Here is a toy example of a program using local services (you can of course split this in multiple files; oh, and I'm omitting what happens in case we do not find some records or files):


service Images {
    Interfaces: ImagesIface
    main {
        get( request )( img ) {
            fetch@ImageDB( request )( img )
        }
    }
}

service Products {
    Interfaces: ProductsIface
    main {
        get( request )( response ) {
            query@ProductDB
                ( "select name,desc,imageurl from products where id=:id"
                    { .id = request.id } )
                ( response )
        }
    }
}

main {
    viewProduct( request )( product ) {
        get@Products( { .id = request.id } )( product );
        if ( request.embedImage ) {
            get@Images( { .url = product.imageurl } )( product.image )
        }
    }
}

The program above is a simple service to access product information. It offers one operation, viewProduct, that clients can invoke with a message containing the id of the product they want to see (as a subnode of request). The service then invoke the internal service Products, which queries a database for some basic information and returns it. The product information contains an URL to an image depicting the product. Then, the main program checks whether the original client requested the image to be embedded in the response (useful, e.g., for mobile applications that want to minimize the number of message exchanges); if positive, the image is added to the response by fetching it from the Images service.

The main service, Images, and Products communicate using shared memory in the Jolie implementation of internal services. Communications are therefore always going to succeed, but each service has its own interface and state and the style is still message passing. Hence, internal services can be used to teach how to design a loosely-coupled architecture.

...and MSAs later

NMSAs are expressive enough to teach, e.g., how to deal with concurrency issues (each service has its own processes), message passing interfaces, and good API design. They are also fairly easy to achieve, relieving beginners from the frustrations of service deployment.

After a while, however, an NMSA that needs to scale has to evolve into an MSA, by taking some internal services and making them distributed. Distributing an internal service in Jolie is easy. Simply put, take the code of the internal service you want to distribute and execute it with a separate interpreter in another machine (Jolie Enterprise automatises this job for you in a cloud environment, but you can use the tools of your choice).

The main service now needs to be updated to refer to Images and Products as external distributed services:

outputPort Images {
Location: "socket://images.example:8080"
Protocol: sodep
Interfaces: ImagesIface
}

outputPort Products {
Location: "socket://products.example:8080"
Protocol: sodep
Interfaces: ProductsIface
}

main {
    viewProduct( request )( product ) {
        get@Products( { .id = request.id } )( product );
        if ( request.embedImage ) {
            get@Images( { .url = product.imageurl } )( product.image )
        }
    }
}

Basically, we have replaced the code for each internal service with a respective output port. The rest of the code does not change (again, a key aspect of developing services with Jolie is that deployment is kept as separate as possible from behaviour). However, now the system is distributed. Among other consequences of this, communications with services Products and Images may now fail! In practice, this means that the invocations get@Products and get@Images can throw network-related faults now, and we must account for that. If we leave the code like this, such faults are going to be automatically propagated to clients. This may be undesirable. A better strategy could be:

  • If we fail to get the product information, we immediately return an informative fault to the client.
  • If we manage to get the product information but fail to get its image, we should return at least the product information with a placeholder blank image.
We update the code accordingly, using dynamic fault handlers:



main {
    viewProduct( request )( product ) {
        scope( s ) {
            install( default => throw( ProductsCurrentlyUnavailable ) );
            get@Products( { .id = request.id } )( product );

            install( default => product.image = BLANK_IMAGE );
            if ( request.embedImage ) {
                get@Images( { .url = product.imageurl } )( product.image )
            }
        }
    }
}

There we go. Now our code accounts for network errors (actually any error, since I used the default handler, which catches any fault). There are, of course, many other things to watch out for, e.g., distributed monitoring, crash recovery, and performance. However, this small example gives already an idea of the overall methodology.

Gotcha

Although Jolie does its best in not allowing resource sharing among services (they simply cannot be shared, by the language syntax), this can always be circumvented via external side-effects. For example, two internal service may share access to the same file system. However, this can also be abstracted from: in Jolie all accesses to the file system happen through the File service from the standard library which, being a service, can also be distributed.

Conclusions

The microservices style arises out of practical needs, and although it should be used only when necessary, sometimes it is appropriate. I believe that we should find ways to guide a smoother transition from monoliths to microservices, and that finding such ways is possible. This is a rough attempt at giving a first piece of the puzzle towards building such a transition model.


PS: I should really try to come up with better acronyms.
PPS: If you have interesting examples to share in other languages, please feel free to do so!