It’s impossible to ignore the topic of microservices today. We hear about them from social media, conferences, specialized websites, books, framework vendors, communities, colleagues. It seems like they’re the only way to go.
But are they? To answer this question, we first need to explore why are microservices so useful, what are their downsides and what are the alternatives.
Using Conway’s Law for fast development
organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations — Melvin E. Conway
Conway’s Law has been first stated in 1967, and it has been proven empirically again and again. To understand it, let’s discuss a few examples of its application:
- Teams who can communicate easily (typically small and collocated) tend to create monoliths
- Teams who communicate through intermediaries (managers, architects etc.) tend to work on separate modules
- Teams who communicate through hierarchies tend to work on complex structures. For example one core / framework team, front-end teams, back-end teams etc.
Fred George started one of the first microservices projects with a big challenge: the work had to be organized in a way that allowed adding developers fast. He decided to use Conway’s Law to their advantage, and create an architectural structure that allowed adding pairs of programmers to the development. The only option was to create many small services that communicated asynchronously and that were completely independent of each other, each developed by a pair of programmers. Enter microservices.
A few rules for microservices
According to Fred George, they followed very specific rules for the architecture of the microservices. They started with a long session of domain modelling (about two weeks), that resulted into bounded contexts* and then into well-defined services. Each service:
- uses its own database
- depends only on a message pipeline, never directly on another service
- when needing something from another service, posts a message to the pipeline describing the need
- periodically reads the messages from the pipeline and decide if anything needs to be done
- self-monitors: if something doesn’t work, report and try to self-heal
- is deployed separately
- when changed, a new service instance is deployed. Once the callers switched to the new instance, the old instance can be deleted
In addition, code duplication between services is ignored. Otherwise, libraries will be extracted that increase deployment complexity and can lead to dependency hell.
These constraints allow for independent development by a lot of programmer pairs. Architecture and Conway’s Law work hand in hand, helping the development go faster.
Why are microservices useful?
In conclusion, microservices are useful because of two things:
- scale up the team very quickly by taking advantage of Conway’s Law
- modular development
In addition, microservices can scale very easily and minimize spending on the cloud. The reason is that when load increases, more specific instances can be started at a minimal cost.
As we can see from the industry examples, these criteria apply very well for large projects that need to scale quickly. So if you need to start such a project, by all means, use microservices. But if you don’t need to scale that quickly, there are easier ways. The reason is that microservices, like any architectural approach, are a trade-off.
A downside and a huge mistake
Microservices have one important downside and can lead to one huge architecture mistake.
The downside is that microservices architecture leads to huge complexity increases for deployment and operations. Deploying one monolith is very different from deploying 100 or 1000 separate services. Monitoring and de-bugging then become a very difficult job. There are solutions, but each adds to costs and complexity. This investment might not pay o for smaller products.
The architecture mistake is to create microservices that directly depend on other microservices. This is just a re-creation of the dependency hell problem in deployment. If dependencies are hierarchical, changing a service propagates and require a lot of redeployments. Instead, aim for services that completely encapsulate one atomic change. This is the hardest thing to do about microservices; no wonder it took Fred George’s team weeks to figure it out (and probably a lot more time after starting the development).
There is an alternative to microservices, one that we used successfully for eventriX: the modular monolith.
The idea of a modular monolith is to preserve the idea of encapsulation from above but deploy it in a different manner. We don’t have to deploy a module as a service until we need to scale it. Instead, modules can be deployed as libraries, plugins, namespaces etc., resulting into one file that can be very easily deployed, monitored and debugged. As long as we follow a good modular structure inside the monolith, we can easily extract a service.
So how can we structure such a monolith? It obviously depends on the application. In the case of eventriX, we follow a few clear rules:
- there are three large bounded contexts: speaker, event organizer, and reviewer. More will appear in time.
- each of the bounded contexts has its own URL mapping: “/s” for speaker, “/o” for organizer, “/r” for reviewer
- the bounded contexts don’t depend on each other. When changing some- thing for speaker, nothing changes for organizer. This means that some views, although containing the same information, are duplicated between speaker and organizer. That’s acceptable because speaker views can change with a different speed and in a different direction than organizer views
- each request goes through a clear set of classes: a controller, a command object that validates the request, one or more queries, one or more application service etc. It’s very easy to follow the path of each request, and the paths are different for each request
All these rules allow us to quickly extract separate services or plugins if needed while maintaining the operational complexity at a minimum.
We have found microservices to be a great idea for large projects that need to scale quickly and save money when the load increases. But they also add huge operational complexity. We’ve also seen that the most complex part of developing microservices is getting the modularity right. But teams who master modular architecture have more than one option.
A modular monolith or a well-thought combination between plugins, modular monoliths and libraries allow fast development while maintaining low complexity. They also allow evolving to a more appropriate architecture when the need appear since logical modules can easily be extracted into physical modules.
The decision between microservices and modular monolith is not easy. It requires careful consideration of the trade-offs and of the context. It’s up to the person(s) playing the role of the architect to end the best way. We hope this article helps you reach a decision.
Modularity is a core module (pun intended) of our Software Architecture Principles workshop. If you need something specific related to architecture, let us know.
*Bounded contexts are a core pattern in Domain Driven Design, grouping related domain entities
2 thoughts on “Modular Monolith Or Microservices?”
Where are the downsides of the modular monolith approach?
The typical downside is that it requires discipline from developers. It’s very easy to call classes and functions that are in another namespace; it’s more difficult to maintain the discipline of calling only the entry points to the modules inside a monolith.
I’ve seen however how code review policies and checklists can solve this issue, with a lot less cost than the operational overhead of microservices.
This is not to say that microservices are not good. It is however important to understand where they fit in the architectural landscape.