Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Between Monoliths and Microservices: A Third Way

DZone 's Guide to

Between Monoliths and Microservices: A Third Way

Looking at the differences between monolithic and microservice architectures can help us understand that they're not black-and-white and help us choose how to develop apps.

· Microservices Zone ·
Free Resource

Monoliths versus microservices is a very hot topic nowadays for programmers, engineers, and architects. The more we investigate these two approaches, the more we realize that when building an application from scratch, we need to select one of them from the beginning. I think that more or less everybody is interested in the microservices approach and everybody would like to use it. It is suitable to be used in a cloud-based scalable system, and it allows us to grow the system without taking too much care about the infrastructure we choose. But a rational investigation about the price to pay in terms of complexity, design, and maintenance gave us no real choice to make: the monolith wins. But, some months later...

This is today's "to be or not to be" dilemma of software engineering.

Everybody has their own reasonable opinion about it. But, what happens if we discover that such an issue does not exist? What happens if we find a way to start development as a monolith, which permits us to distribute its components later, depending on our needs? An approach where you just write few lines of code to reconfigure the overall system and change from a monolith to a microservice system, and vice versa? Is it possible?Image title

The Price of Transforming Apples Into Pears

Behind every frustrating dilemma, there is always a revolution in the mindset which will reveal the illusion and melt the dilemma away. What is the illusion here? The illusion is that it is not possible to build a monolith as a composition of distributable message passing components. Such an illusion comes from the fact that usually we address programming starting from a paradigm focused on computation instead of communication. We use to focus on logic and then consider component distribution and publication as a consequence. As an example, consider the case of a piece of Java code transformed into a REST service. We are going from an object-oriented component to an HTTP/JSON-based message component. We are transforming apples into pears. Within the component (the apple) we need to reason by using an object-oriented approach, whereas outside it (the pears) we need to reason in a service-oriented way. But such a transformation is not free. We need to introduce specific software layers.

Image title

Change Your Mind

What happens if we change our minds and consider communication first and computation as a consequence?

Image title

In such a case, all the entities we are dealing with natively communicate by using a message-passing paradigm. The size of a component does not matter, because it is always a distributable message-passing component. It is always a service. If we accept this idea, we should accept the idea that each system we create is inherently distributed. Thus, our initial dilemma of building a monolith or a microservice-based system disappears because it becomes a simple problem of deploying services separately or together into a single application.

Image title

A Linguistic Approach

I am sure that now you have the intuition and you are trying to figure out how to achieve this by using some kind of framework — but, we do not want to use frameworks because we do not want to pay the overhead of their introduction. Here, the main idea is to jump towards a new generation of programming languages which crystallizes the basic concepts of service-oriented computing into syntactic constructs, a generation of programming languages which are focused on communication and allow us to program distributed applications following an intuitive approach.

Jolie

Jolie is the programming language we have been working on since 2006. It was initially conceived as a formalization of SOA standards like WS-BPEL and WSDL, but it immediately showed us that it is an innovative technology for dealing with distributed programming based on services. Microservices is a keyword which came about after our first release, and we when we discovered it, we saw a lot of the powerful concepts we discovered in Jolie. The only difference was that we had them in one single language instead of a mix of technologies. Jolie has its own syntax, and the actual engine is an open-source project developed in Java. Unfortunately, there is not enough space here to discuss all the features of Jolie. Let us just show you why we don't experience a dilemma between monoliths and microservices with Jolie.

An Example in Java

Let us consider a very simple example: a calculator which provides one single method for performing an arithmetic operation and selects the related implementation depending on the selection. Let's do this in Java where there is a class called Calculator, which provides a static method, calculate, and then selects the possibility to perform a sum or subtraction depending on the parameter operation.

public class Calculator
{
public enum CalcType {
SUM,
SUBT
}

public static int calculate( int x, int y, CalcType operation ) throws Exception {

        switch( operation ) {
            case SUM:
                return new Sum( x, y ).execute();
            case SUBT:
                return new Subt( x, y ).execute();
            default:
                throw new Exception("OperationNotSupported");
        }
    }
}

Very simple. The classes Sum and Subt implement an abstract class called OperationAbstract, reported below:

public abstract class OperationAbstract {
 protected int X;
 protected int Y;
 public OperationAbstract(int x, int y) {
  X = x;
  Y = y;
 }

 abstract int execute();
}

What happens if, for any reason, we need to extract the class Sum and deploy it externally as a REST service? Which framework will we select to deploy it? Will we provide a Swagger interface? And what happens to the call inside the Calculator class?

An Example in Jolie

First of all, take note that in Jolie, everything is a service, so the components in charge of calculating the sum and the subtraction must be services. Thus, let us introduce the interface they need to implement. It is comparable with OperationAbstract in Java even if it is a service interface.

type ExecuteRequest: void {
   .x: int
   .y: int
}

interface OperationServiceInterface {
RequestResponse:
execute( ExecuteRequest )( int )
}

The interface is called OperationServiceInterface and it provides a RequestResponse operation called  execute. A RequestResponse operation is an operation which receives a request message and replies with a response message. In this case, the request message type is defined by ExecuteRequest, which contains two subnodes: x and y. Both of them are integers. On the other hand, the response is just an integer. Now, let us see how a service which implements this interface looks:

include "OperationServiceInterface.iol"

execution{ concurrent }

inputPort Sum {
    Location: "socket://localhost:9000"
    Protocol: sodep
    Interfaces: OperationServiceInterface
}

main {
    execute( request )( response ) {
        response = request.x + request.y
    }
}

Note that the keyword highlight is pretty ugly because the Jolie syntax does not exist on DZone.

Row 1 means that we are including the file where OperationServiceInterface is defined. Inclusion is just a way for better organizing code into separated files. In row 3, the directive execution{ concurrent } states that all the initiated sessions must be executed concurrently, thus the service is able to serve several calls simultaneously. At rows 5-9, we declare the listening endpoint (in Jolie it is called inputPort) where we receive messages for the service Sum. Note that an inputPort requires a Location, that is where the message must be sent, a Protocol, that is the way a message is sent (sodep is a binary protocol you can use between Jolie services) and the Interfaces available at that endpoint. Finally, the scope main at rows 11-15 defines the code to be executed when a message is received on operation execute. The incoming message parameters are stored into the variable request, whereas the variable response holds the reply which will be automatically sent when the scope ends. The service Subt is identical with the exception of the body of the operation execute and the Location which will be different because the two services are independently deployed.

Now we need a service which plays the same role of class Calculator and that finalizes the architecture, as follows:

Image title

Let us see the code of the service Calculator:

include "OperationServiceInterface.iol"

type CalculateRequest: void {
   .x: int
   .y:int
   .op: string
}

interface CalculatorInterface {
RequestResponse:
calculate( CalculateRequest )( int )
throws OperationNotSupported
}

execution{ concurrent }

outputPort Operation {
  Protocol: sodep
  Interfaces: OperationServiceInterface
}

inputPort Calculator {
    Location: "socket://localhost:8999"
    Protocol: sodep
    Interfaces: CalculatorInterface
}

main {
    calculate( request )( response ) {
        if ( request.op == "SUM" ) {
            Operation.location = "socket://localhost:9000"
        } else if ( request.op == "SUBT" ) {
            Operation.location = "socket://localhost:9001"
        } else {
            throw( OperationNotSupported )
        }
        ;
        undef( request.op );
        execute@Operation( request )( response )
    }
}

Note that in rows 22-26, there is the definition of the inputPort of the service Calculator which is listening on port 8999 where the CalculatorInterface is defined in rows 3-13. Differently from the ExecuteRequest of the Sum and Subt services, here the request also contains the subnode .op:string, which permits to specify the operation type. In Jolie, it is also possible to specify a fault sent as a response, like we did in line 12, where we define that operation calculate can also reply with the fault OperationNotSupported.

In rows 17-20, we define a target endpoint for the service Calculator by means of a primitive outputPort. Usually, an outputPort requires the same parameters of an inputPort (Location, Protocol, Interfaces) but here the Location is omitted because it is dynamically bound at runtime depending on the value of request node op. Indeed, in rows 31 and 33, we bind the port Operation to a different location depending on if we call the service Sum or the service Subt. In line 39, we actually call the operation service which is now correctly bound to the selected service operation. In line 38, we erase the node .op from the request in order to reuse it as a request message for the operation service.

As you can see, such a system is distributed. We could place the three services in different machines on the same network and it would work. But what about our initial dilemma? What about deployment as a monolith? The solution is very simple — we just need to run the services Calculator, Sum, and Subt together within the same virtual machine. In Jolie, we can achieve this by embedding the services Sum and Subt inside Calculator and everything will work in the same way. In the following you can see how the service Calculator must be modified in order to obtain a monolith:

/* interface definition does not change */

execution{ concurrent }

outputPort Operation {
Protocol: sodep
Interfaces: OperationServiceInterface
} 

embedded {
  Jolie:
  "sum.ol",
  "subt.ol"
}

inputPort Calculator {
    Location: "socket://localhost:8999"
    Protocol: sodep
    Interfaces: CalculatorInterface
}

main {
    calculate( request )( response ) {
        if ( request.op == "SUM" ) {
            Operation.location = "local://Sum"
        } else if ( request.op == "SUBT" ) {
            Operation.location = "local://Subt"
        } else {
            throw( OperationNotSupported )
        }
        ;
        undef( request.op );
        execute@Operation( request )( response )
    }
}

The only differences are in lines 10-14, 25, and 27. In rows 10-14, we use a primitive of Jolie called embedding which permits executing external services inside the parent one. In this case, the service Calulator embeds the target services Sum and Subt defined in files sum.ol and subt.ol respectively. In lines 25 and 27, we just specify the new local locations for service Sum and Subt. It is worth noting that, clearly, aslo their inputPorts must be modified coherently. For example, the inputPort of the service Sum now must be:

inputPort Sum {
    Location: "local://Sum"
    Protocol: sodep
    Interfaces: OperationServiceInterface
}

The structure of the software does not change between a monolith or a distributed system.

The most important fact here is that the structure of the software does not change depending on the deployment. Such a result is obtained because of the linguistic approach where we crystallized the core concepts of service programming into a coherent set of primitives. The software is inherently conceived to be distributed and all its components are born as services. Moreover, as the example shows, the effort to be paid in terms of lines of code has the same order of magnitude as the Java case.

In Jolie, the basic unit of programmable software is a service and the programmer can only create services without caring about their actual deployment. For all these reasons, in Jolie, deciding to build a monolith or not is not a choice between life and death, it is a simple deployment choice which can be easily postponed.

Remember That Monoliths and Distributed Systems Are Different

Although that in Jolie the difference between a monolith and a distributed system are very light, we cannot forget that a distributed system is inherently different from a monolith. It is quite obvious indeed, that when we distribute a component we need to take into account the fallacies of the network which are not present inside a monolith where all the interactions are operated in memory. In some cases reliability is so important that we need to introduce extra code for dealing with communication exceptions, by programming recovery activities. In Jolie, such an effort is mitigated because of the powerful fault handling mechanism provided by the language. Moreover, there are specific primitives for dealing with termination and compensation too that are very precious in case of complex distributed scenarios.

Conclusion

With this article, I hope to have shown how a possibility to directly conceive software as a distributed composition of services exists, even in the case of a monolith. If we consider the idea to exploit a dedicated programming language for doing it, our perspective could change so much that the dilemma of choosing a monolith approach or not could disappear. It happens just because the software is already distributed and the monolith is just a way of deploying it.

The project of creating a new programming language like Jolie is very exciting, and it gave us a lot of satisfaction when used in production environments, too. We experimented with all the benefits of natively programming distributed applications and we are very keen about the new possibilities that such an approach raises every day. If you are curious, you can find other information about Jolie on its official website. We are looking forward to your comments and suggestions!

Topics:
microservices ,monolith ,distributed systems ,modularization

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}