Skip to content

Anyone who has built a large application in recent years has probably implemented it using microservices. This architecture style was introduced very successfully in large tech companies such as Netflix, Uber and Amazon, and has triggered a veritable revolution. These big tech companies had thousands of developers all working on the same code base. Large, monolithic applications had become so complex that even the smallest changes required a great deal of effort and there was a high risk that new bugs would have been introduced.

The proposed solution was simple and effective: instead of one large, monolithic application, you have several small, independent applications. These micro-applications are so small that one person can understand them completely. They are so independent that a team can work on them without having to coordinate with all the other teams working on the same system. The promise was that such services could be developed faster and more efficiently than if everyone was working on the same large code base.

Today, the hype has gone so far that a microservice architecture is required even for medium-sized projects, even if only one team is working on them.

The false promise

The intention behind it is good, but for all the promise of making complex systems manageable with a microservice architecture, the problems that such an approach creates are often downplayed. Google addressed the top five problems in a paper published in 2023:
 

  1. A microservice architecture is often slower, as the individual services communicate with each other via a network, and data has to be serialized multiple times. This becomes a bottleneck, especially if there are many microservices.
  2. In a system, several microservices have to work together and communicate with each other. These microservices are usually not as independent as they appear in theory. It is difficult for the overall system to assess whether the services used are working correctly together in the version used, even if the individual services are working correctly.
  3. The management of the system is more complex. Someone needs to clarify which version of which service is compatible with the other services and should be used. Integration tests are needed with all services. Due to dependencies, updates to one service also require updates to other services. New, complex technologies are required for the deployment and operation of distributed systems. Finding errors in a distributed system is much more complex because there are more moving parts.
  4. Developer teams have to negotiate the interfaces among themselves. Not just once, but again with every adjustment. This negotiation of interfaces significantly reduces the speed of development, especially at the beginning of a project.
  5. It also causes interfaces to become static very early on. This means that potential improvements are not implemented or are only implemented much later, either because several other teams are already using the interface or because you don’t want to renegotiate with another team.
     

Modularity also works differently

Some of this unwanted complexity can be eliminated by packaging all microservices in the same code repository and always publishing them as a unit. A further reduction in complexity is achieved if the services do not communicate via a network, but via an internal bus.

Such a structure is called a modular monolith.

This uses the concept of modularization from the microservice architecture, but keeps everything in one code base. Individual modules may only communicate with each other via defined interfaces; transactions across module boundaries are not permitted, as is the case with microservices. An automatic check enforces compliance with these rules.
The only thing you lose from a technical point of view with this approach is the dynamic scaling of the individual services. But that shouldn’t matter until you reach your first million users. If necessary, a module can easily be outsourced to a separate service later.

Because all components are based on the same code base, a team can adapt the corresponding interface in the code of another team at the same time if an interface is changed.

Deployment is also atomic: all modules are published together. This means that there is no need to manage compatible versions because there are no different versions.

Another important point with this architecture is that, unlike microservices and classic monoliths, there is no 1:1 relationship between code and deployment. Several services can be built and deployed from one code base.

The popular Java framework Spring offers support for this approach with the Spring Modulith module. It offers its own event bus for reliable communication between the modules and a test module based on ArchUnit, which prevents modules from accessing the internal code of other modules. It also visualizes the architecture for easier understanding.

The large monorepos of Google and Meta show that such a concept works well. For years, almost all of these companies’ applications have been combined in a single code base, which is several terabytes in size with versioning.

Martin Fowler, one of the early advocates of microservice architecture, said that all the successful microservice architectures he has seen have emerged from a monolithic architecture. Projects that started with a microservice architecture have all run into serious difficulties.

So, if you have a new project, you should carefully consider the option of a modular monolith.