How to Get Rid of Microservices?
![Avatar](https://backendhance.com/img/marcus-held.jpg)
Author
Marcus HeldIn recent years, numerous medium-sized companies have tried the much-touted microservices and found that they are less lightweight than their name suggests – Microservices in SMEs are often overengineering . Now, they need to be deconstructed and transitioned to a more pragmatic architecture. We explain how this can be achieved.
Don’t worry: The deconstruction and transformation of the architecture is not only labor-intensive but also an opportunity. In software development, there is no one-size-fits-all solution; the appropriate architecture for the software is chosen based on the software’s requirements. This involves gathering the requirements of all stakeholders, which is often not adequately done in many projects. An architectural transformation allows for this to be rectified and reviewed.
Changes since the start of the project can also be addressed. Assumptions that were valid at the project’s inception may have changed, and new requirements may have emerged. It’s possible that not all past decisions are still applicable today.
Preliminary Work 1: Check Which Parts of the Software Need to Remain Distributed
Many requirements can be identified through stakeholder interviews. How has the topology changed, i.e., the division of departments? Are there more than one team working on the project? This could be a reason to continue working with a distributed system, where each team is responsible for its own service, making teams independent and self-reliant.
Another, albeit rarer, reason for dividing software into multiple applications is performance. Compute-intensive operations should not disrupt the main application, and outsourcing to a separate application is a good solution for this. These are just two reasons for dividing software into multiple applications.
From these examples, it should be clear: Even if you are deconstructing the microservice architecture due to negative experiences, never be dogmatic. Evaluate which parts of the software should remain distributed.
Preliminary Work 2: Check What Belongs Together
Services that were divided under outdated requirements or incorrect assumptions need to be merged into one application. These services communicate heavily, with nearly every request passing through multiple systems. Data from various sources must be consolidated to handle requests, resulting in a lack of transactional security and inconsistencies. The system reaches its load limit with just a few dozen requests.
These systems are mistakenly called microservice architectures. Upon closer inspection, they are more of a distributed monolith. The services in the system are not independent but a bounded context and should never have been separated. What seemed simple at the beginning of development proves to be a difficult-to-change and error-prone system in live operation.
But where do we start with the deconstruction of the microservice architecture and the transformation to a more suitable architecture?
When transforming a distributed system into a monolithic application, we start where there are the most interactions. A system that has grown over the years cannot be completely transformed in a short time. Therefore, it is important to prioritize from the start and begin where the greatest benefit can be achieved.
The task is to identify which services need to be
merged first to generate the greatest benefit.
Step 1: Determine the Current State
To do this, we record the current state, which can be done by creating an architecture diagram with the team. This diagram will show all interactions between the services. In arc42, for instance, this is Chapter 5: Building Block View ; in the C4 model, it is the System Context and Container Diagram .
The service with the highest coupling is a good candidate for the first refactoring. In an architecture diagram, it is identified by the number of incoming and outgoing arrows, which represent the interactions between the services.
In many distributed monoliths, a central service interacts with almost all other services. This is often a user or tenant service. Such a service must be queried in nearly every system interaction, often multiple times within a single request.
Since this service is so central, it is a good candidate to form the foundation of the future monolith.
The first service to be reintegrated into the monolith should actively cause problems in development. For example, one that hits the load limit due to regular API calls to the user service or a service that frequently needs adjustments for new features. Look for a service whose transformation will directly impact development or the product’s KPIs.
Step 2: Build a Modulith
The term “monolith” has a negative connotation today due to past experiences with large, single applications being heavy, inflexible, and having many side effects.
The term “modulith” was established to escape this negative connotation. It indicates that while many parts of the program are bundled into one application, they are separated by modules.
Various strategies for modularization have been invented and favored over the past decades. In Java, modules have been available since Java 9. The always-present packages are also intended for code modularization. However, both approaches have disadvantages and have not proven to be sufficient solutions for many projects.
Archunit or the newer Spring Modulith project use annotations to describe architectural constraints. Tests in CI ensure these constraints are not violated.
Developers want clear rules. The compiler should prevent accesses that violate the architecture. In a layered architecture, only the business layer should access the persistence layer. The desire for clear rules makes the microservice architecture attractive as it represents the most consistent separation of modules.
The desire to enforce architecture as strictly as possible comes from an experience nearly every developer has had: the big ball of mud, the dreaded spaghetti code. Therefore, the monolith also needs clear rules and boundaries.
An alternative to modularization is the separation of source code by the build tool. In Maven or Gradle, a project can be split into multiple sub-projects representing an application module. Dependencies between modules are declared as dependencies, preventing unintended couplings between two modules. The compiler will not allow architectural violations.
There are two basic approaches to cutting modules: by function or by domain. The classic layered architecture is a functional cut, grouping classes with similar roles together. For example, all classes accessing the database are in the persistence layer.
Advantages of this architecture are low cost and easy understanding. Developers find it easy to decide which layer the class being worked on belongs to. Also, the architecture can be easily tested and secured by rules.
However, the larger the codebase, the more problems arise with the layered architecture. It becomes harder to identify classes that belong to a feature, increasing the likelihood of side effects and delaying development. This experience has contributed to the popularity of microservices and domain-driven design (DDD) in recent years.
If we model our modules with bounded context as described in DDD, we can still split the application at a later stage. This maintains flexibility to adapt the application to future organizational conditions.
Additionally, we increase the maintainability of the application. Unlike the functional cut, DDD-modeled modules contain all necessary classes side by side.
The approaches to separate the modulith by DDD and layers can be combined. In this scenario, modules are modeled using DDD, and within the modules, the layered architecture ensures data flow.
All techniques discussed are combinable.
I recommend splitting modules with the build tool. This way, I don’t get suggestions in my IDE’s autocompletion for classes I shouldn’t use. Within a module, data flow can be modeled with a tool like Archunit or Spring Modulith, enabling quick and straightforward adjustments if new layers are added within the module.
TL;DR
Each project is different. However, the experience of the past ten years has taught us that microservice architectures are not a solution for all problems. Especially medium-sized companies rarely have the necessity and resources to handle such a complex architecture. These companies should opt for lighter architectures. The modulith is an adequate alternative.
When transforming to a modulith, we start with the services that have the most interactions in the system. Then we ensure with one of the presented techniques that an incorrect access between modules is not possible.
We should always be aware: Every architecture has advantages and disadvantages. Therefore, we should bring together the experts who can discuss these pros and cons and make the best decision based on this discussion.