Microservices force developers to loosely couple their services from the start, but definitely come with an overhead in terms of development time and cost.

When developing microservices programmers need to find a way for them to communicate, share user data and authorize actions, deal with network problems, testing the whole application, distributed transactions and many more challenges. Despite there are many great frameworks available for creating distributed systems such as Spring Cloud, dealing with these problems and getting the whole system up and running can definitely slow down developers in the early days of the project. When a project starts the primary goal is usually validating the business idea, by delivering an MVP as soon as possible to a limited amount of users.

Monolith comes to the rescue?

By choosing a monolith architecture the MVP can be delivered faster, we spare the overhead of working with distributed systems, but what happens after the MVP is successful? We might reach a point where we need to scale specific parts of the application horizontally, deploy some feature specific updates faster or split the code base for developers to be able to work more efficiently. That is where switching to microservices should be considered. If the monolith is not structured well and all parts of the application are tangled and interconnected, the switch might come with a full rewrite increasing development cost painfully.

A modular monolith architecture

Breaking down a modularized monolith to microservices is much easier, generally the new microservices will contain one or more of the existing modules. The modules already have their borders, interfaces and DTOs, sparing developers the tedious work of untangling the whole application. Of course the challenges of distributed systems mentioned before are need to be dealt with, but it will feel like adding new properties or features to an application not entirely rewriting one. So it is a good idea to use modules in monolith applications from the start of the projects. The following diagram shows and example of how to do that. This example architecture assumes that we use some sort of a web / dependency injection framework like Spring.

High level architecture

The core module is the home of all framework specific high level configuration, e.g., security, datasource, monitoring. The core module also contains the main() function of the application, which is responsible for starting the application and its context.

The feature modules (A and B) contain all the features of the application. They should be organized in a way that code which change together belong to the same module. Feature modules can of course depend on each other if needed, but watch out for circular dependencies. The core module will depend on each of the feature modules, at least transitively, to make dependency injection provided by the framework work and be able to build the application’s context.

Splitting to microservices

Let’s say we want to split the system in the previous diagram to two separate microservices. The first step would be creating a new microservice with a new core module. If some configurations need to be shared between the two modules and we suspect that they will change together in the future, it is a good idea to create a shared module on which all the core modules will depend on.

Splitting to microservices

The next step is moving the B module to the new microservice. We can do that easily since the modules are already quite separated. The B module depended on the A before and it will continue to do so, but communication between them will transfer from traditional function calls to network calls. If using HTTP for communication, this means we need to introduce some new controller classes on the A service’s side to make our existing service interfaces available through the network and call these endpoints in the B service. Always keep in mind that network requests can fail easily.

Disadvantages of not using modules from the start

Not using modules from the start

If modules are not used from the start and some day we start introducing them, we will have a “fat” core module which contains what a core module should and some features, developed before introducing modules, on the side. The core module must depend on the other modules to make dependency injection work, but some newly introduced feature modules are likely to depend on some features put previously in the core module. This can easily lead to circles in the dependency graph which should always be avoided and can make building the application and creating the context really hard.

The potential problems can be avoided by extracting features from the core module into separate feature modules, but at this point it can be a really challenging and will be a lot of waste of time.

Conclusion

Going for microservices instantly at the start of the project may not be the best solution, especially if the business idea has not been validated yet. By going monolith we can usually ship the MVP faster, but we must keep in mind that some day we might need to move away to microservices and with the right foundations, by making our monoliths modularized from the start, it can be a much less overwhelming task.