Case Study: Ditching Microservices in Favor of a Modular Monolith

02 January 2020
By Aleksei Loos, Software Architect
Share this article:
Aleksei Loos

When I joined the Software Architects team in 2014, I naturally started exploring all the IT buzzwords out there. The concept of microservices was definitely one that caught my eye as it was around the same time when it started gaining public traction. For example, initial versions of Docker, Kubernetes and Spring Boot were published around that time. It seemed that microservices were suddenly considered as a silver bullet solution to all the problems that modern IT cannot function without.

But what did we have before microservices became widespread? Monoliths! The JAR hell of dependency management and uber-jars. It should be a familiar feeling for the developers when an application crashes because you forgot to upload a new JAR to the library folder.

When creating a monolithic server, a lot of tools generate a clean, horizontally layered architecture for the developers. For example a common pattern is separating the presentation, business and persistence layers. If domain driven design concepts are applied, the end result will also have logical domain silos. This should be a clear indicator that not all monoliths are inherently bad.

The problem with monoliths starts if the business logic becomes spaghetti. With ever-growing requirements, the cross-domain dependencies are easy to happen. It’s much easier to implement a flow via an internal method call compared to some cross-network REST API.

The fun begins if both, monoliths and microservices, are combined into a single concept. This is called modular monolith and there are some clear benefits:

High cohesion: Modular code that is straight to the point, no side-effects since it’s supposed to work only in its own domain. Comparable to a single microservice instance – it serves only the business logic from a single domain.

Low coupling: A module should not care which other modules are available, other than the required ones. This is comparable to microservices’ service discovery – a service asks what it needs and it receives an endpoint for consumption.

Code isolation: There are many options out there but code isolation can be easily enforced with build tools such as Maven or using Java 9’s named modules approach.

Stable dependencies: By hiding the implementation, the result is an API that is more stable and changes less frequently. Even if it does change, applying updates within same application/monolith is easier.

ACID vs. BASE: When using relational databases, one of the main benefits is transactional data updates. In case of microservices, there is a need to take into account possible network problems and it's often compensated by reimplementing in code the logic that our databases do anyway.

As part of the continuous refactoring process in our Playtech Casino backend team, we have ended up with around 6-8 smaller domains so far. During the business project analysis phase, we identify the impact on the existing monolithic code. Based on the findings, a decision is taken if the impacted logic will be extracted and encapsulated into a separate domain.

From architect’s perspective, the desired solution would be delivering each domain as a microservice. This is supported by the fact that in Playtech there are multiple departments running microservices and cloud-native applications. Yet from a single team perspective, maintaining all those domains as microservices seemed like an unnecessary overhead.

After a study of different options, we decided to combine the extracted domains as isolated modules within a single server. Later, one of our senior developers sent me a Youtube video where the concept of modular monoliths was explored. The sudden realization that we followed the same pattern reassured our earlier decision.

As a result, the Developers’ team is happy since they can get the code from a single repository, run the whole logic by just starting the server locally and work almost independently on the modules that are relevant to the current project.

The QA team is happy since the developer impact report usually has the clear isolation and they need to crash less servers to find the edge-cases.

The Release team is happy since they have to write less deployment guides and manage less server components.

The DevOps team is happy since there are less servers to take into account during troubleshooting and reporting defects is easier since it all happens in one place.

The Infra team is happy since there is no need for additional orchestration middleware.

The Architects’ team is are happy since we can create new services quite easily, keep domains separated and enjoy the service-oriented architecture.

Casino team at Playtech Estonia

Microservices are still great, but their usefulness comes down to the context. They are usually advocated by big companies with thousands of developers and this concepts fits them nicely - lots of teams with their own dedicated responsibilities while using the services of other teams.

M. Conway, a computer scientist, once said: “Organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations.” This is a clear guideline for architects to take into account the company structure when designing new solutions. When ignored, there is a risk of creating increased maintenance complexity for the product and you could end up watching your team break apart over time due to subtle misery of the members.