It is always fun to start a new project. Fresh and simple classes, clear boundaries, clear architecture – everything is logical and beautiful. New functionality is added quickly and easily.

Time passes, the project grows and new requirements keep arriving. So the day comes when you find something bad in the code. Someone cut a corner and made a small glitch. It even happens that you yourself do something in a hurry, honestly inserting “still todo” into the code – simply because this functionality is needed for the upcoming release, and there is no time to do everything correctly. “We know it’s there, but we will definitely fix after this release, but now we need to issue this version,” is usually said at that time. It’s a technical debt.

However, there is nothing more permanent than temporary! Usually it happens that the technical debt is only increasing. Just as a loan in a bank requires interest payments, the presence of technical debt takes its interest. We pay for this with the increasing complexity of making changes, the increasing non-obviousness and illogicality of the model, the disappearing enthusiasm of the team.

Then at a certain point, it becomes clear: The story repeated itself and we have in our hands the next “big ball of mud”. What to do and can this be avoided?

Nowadays, many see the answer in microservice architecture. It has clear physical boundaries, which will not allow cutting corners. Not at least in the way it would have been done in the case of a monolith system.

But the microservices approach has its price, stemming from the distributed nature of such a system. Where everything happened before in one process, we now have inter-server interaction. Which comes with data transmission over the network, as well as serialization/deserialization of data. Where there used to be transactional integrity, now there is event consistency. Instead of synchronous calls, with a clear result, now there is an asynchronous call to several nodes, each of which may return with an error or might give a timeout. And many other issues that are not obvious at first glance, but will automatically pop up because we are using distributed systems. Even when starting a new project, it may be difficult or even impossible for us to properly divide the future system into microservices, simply because we do not know how the system will develop. And trying to think about the architecture in advance is usually unproductive.

In general, when developing a new project from scratch based on microservices, the feeling of shooting flies with a cannon does not leave. The system still does not look so complicated as to apply division into subsystems.
Obviously, when the system starts to grow there is a moment when the price of microservices pays off. It will be reducing the costs of decreasing the productivity of the team when the system becomes more complex. Martin Fowler (Martin Fowler) well illustrated this in his article Microservice Premium:

It is also clearly seen that for projects of small and medium complexity, the monolithic approach is more advantageous in terms of team performance. But this is precisely the stage when it is often determined whether the project will get a larger life. For example, in the case of a startup or a proof-of-concept project. Spending resources at this stage, taking them away from the development of features can lead to the fact that this great life will not come. And if the project does not take off, then the resources will simply be thrown away.

Now, if it were possible to start developing a monolith system at low and medium complexity of the project, and then jump onto microservices. Using microservices to continue to develop a system of great complexity.
Unfortunately, this approach has not yet been invented. But on the other hand, there is another one – which may well claim to be the “golden mean”. The discussion below will deal with modular organization and related benefits.
If you try to compare the modular and microservice approaches, you can select the following characteristics:

 

Characteristic      Modules     Microservices
Decomposition and strict boundaries + +
The possibility of having your own database + +
Easy refactoring and border transfer +
No networking overhead +
No infrastructure overhead +
Typing and checking compiled data +
Check component dependencies at compile time +
Maintaining the scheme of interaction of components in an explicit form +
Synchronous interaction +
Ability to perform transactions between components +
Using different languages ​​for different components +
Independent deployment / split processes (~ fault tolerance) +
The ability to independently scale components +
Different life cycle for different components +

So, we want to divide our systems into parts, defining interfaces at the boundaries that will provide contracts for interacting components. In principle, we could try to introduce the appropriate package division (java packages). But it will quickly become clear that this will not help in keeping the boundaries, since a sufficient level of encapsulation is not provided. You can always refer to any public class to bypass the interface. And with the help of the reflection API, you can even access private methods / fields. This leads to the fact that parts of the application are no longer interacting on the basis of contracts, but on the basis of knowledge about the internal structure of each other. Moreover, any change that does not even change the contract component will break all the components depending on it.

How to provide strict encapsulation? On the one hand, for the Java platform, there has long been an industry standard for the component architecture of the OSGi application, which solves this problem and even allows dynamic loading / unloading of components without restarting the JVM. On its basis, a number of systems were already implemented. However, over two decades of its existence, OSGi hadn’t gained popularity, most likely because of its complexity, which does not allow for drastically reducing the costs compared to the same things implemented through microservices.

The good news is that within the framework of  Java 9,  a modular system at the JVM level has been implemented. On the one hand, this allowed the Java platform to be modularized, and on the other hand, it provided a design that ensures strict observance of the module boundaries both during compilation and during execution.

In general, with the correct application of this approach, it will be possible to call modules as microservices without HTTP and inside the JVM. For such a system, apparently, the picture will be valid:

At the same time, nothing interferes at the later stages, when the borders of the modules have already been tested by reality, to segregate them into standalone microservices. Thus, you can get the advantages of both approaches – a quick start to the monolith with a linear increase in the complexity of the microservice system.