Service Decorators
TL;DR: decorators for services are pretty easy to write in Jolie, thanks to a native feature called aggregation. Be careful in the deployment phase for managing performance hits!
Service Decorators
Just read this article about using decorators over inheritance in object-oriented programs. I assume that you know decorators in the following. If you don’t, get informed (e.g., by reading that article) before proceeding.
The Decorator design pattern offers a way to cleanly modify the behaviour of an object by using composition instead of inheritance. I won’t enter in the merits of composition against inheritance in object-orientation (both have their own, depending on the other features of the language), because I’m interested in microservices here. In microservices, composition is typically the only option you have. That’s because your microservices may be written in different languages, or even paradigms. And even if their codebases were somehow compatible, inheritance would still be out of place: your microservices can only depend on the communication APIs of other services, not on their implementation details (e.g., by looking at their source code).
Since decorator acts through composition, this pattern is potentially interesting for service programming. But as the author of the article linked above mentions, using it can be frustrating since it requires a lot of boilerplate code. This makes it also error-prone. As a running example, let’s look at the e-mail service interface proposed in that article, rewritten in Jolie:
interface EmailServiceIface {
RequestResponse:
send(Email)(void),
listEmails(Range)(EmailInfoList),
downloadEmail(EmailInfo)(Email)
}
Decorating an operation
Now, suppose that this service is available to us as EmailService
. Assume also
that we want to write a new service that decorates EmailService
with the
following logic: whenever send
is called, we check if we have sent an
“important” e-mail (e.g., the subject contains a specific keyword telling us
that the e-mail is important, or the addressee is in a special list); if an
important e-mail has been sent successfully, we backup its content by calling
another service, for indexing and safe-keeping.
A naive implementation is to write the decorator by re-implementing interface EmailService
, like this:
inputPort EmailServiceImportant {
Location: MyLocation Protocol: MyProtocol
Interfaces: EmailServiceIface
}
main
{
[ send( request )( response ) {
send@EmailService( request )();
check@Important( { .subject = request.subject, .to = request.to } )( important );
if ( important ) {
backup@BackupService( request )()
}
} ]
[ listEmails( request )( response ) {
listEmails@EmailService( request )( response )
} ]
[ downloadEmail( request )( response ) {
downloadEmail@EmailService( request )( response )
} ]
}
The code for listEmails
and downloadEmail
is boilerplate, since we’re just
forwarding requests and responses. The author of the article
suggests that it would be nice if languages supported a native feature that makes
writing this code unnecessary. Luckily, we have it in this case!
Forwarding is a native feature in Jolie, since building proxies is the bread and butter of service composition (think of load balancers, caches, etc.). We can rewrite our decorator like this:
inputPort EmailServiceImportant {
Location: MyLocation Protocol: MyProtocol
Aggregates: EmailService // Aggregates instead of Interfaces!
}
main
{
[ send( request )( response ) {
send@EmailService( request )();
check@Important( { .subject = request.subject, .to = request.to } )( important );
if ( important ) {
backup@BackupService( request )()
}
} ]
}
No boilerplate, same behaviour as our previous decorator!
Interface Decoration
What if you want to change the behaviours of many operations, not just one? What if this behaviour change is always the same? For example, suppose that we want to write a decorator that keeps track of all events: whenever an operation is called, we write this in an external log.
Here’s a naive implementation:
inputPort EmailServiceLogger {
Location: MyLocation Protocol: MyProtocol
Interfaces: EmailServiceIface
}
main
{
[ send( request )( response ) {
send@EmailService( request )();
log@Logger( request )()
} ]
[ listEmails( request )( response ) {
listEmails@EmailService( request )( response );
log@Logger( request )()
} ]
[ downloadEmail( request )( response ) {
downloadEmail@EmailService( request )( response );
log@Logger( request )()
} ]
}
Ouch, boilerplate again! What we need is the capability of writing that logging
code just once, and applying it to the entire API of EmailService
. That’s what
courier processes
in Jolie are for. Here’s the improved code, using courier
:
inputPort EmailServiceLogger {
Location: MyLocation Protocol: MyProtocol
Aggregates: EmailService // Aggregates again
}
courier EmailServiceLogger // courier enables primitives for whole-interface behaviour
{
[ interface EmailServiceIface( request )( response ) ] { // Apply to all operations in the interface
forward( request )( response ); // forward is a primitive: it forwards the message to the aggreated (in this case, decorated) service
log@Logger( request )()
}
}
No boilerplate again!
Circuit breakers
What’s a cool example of a decorator? Circuit breaker! Yup. A sketch in Jolie using aggregation can be found in this paper.
Conclusions
As cool as decorators are, don’t forget that you are adding a layer of
indirection. Many times, you don’t have a choice and your code benefits so much
that you should pay the price. But in microservices, be very careful about how
efficient your extra layer will be. If you stack too many decorators and they
are all communicating remotely via sockets, you’ll soon have a performance
problem. This is a deployment challenge: in Jolie, using a different
communication medium doesn’t alter your behavioural code. So choose your
communication media wisely when you deploy! If you have control over your stack
of decorators, consider whether it would be better to put them all in one place and
have them communicate using shared-memory (local
location in Jolie).