Monolith vs Microservices : From the team’s perspective
By: Nayab Siddiqui
Tags: Startup, Scaling, Technology, Architecture, Microservices

Glossary

Let’s begin by collectively visiting some of the terminologies used in this writeup. If you’re already familiar with them, jump straight to Considerations section.

Modular Monolith:

While referring to a monolith in this article, I’ll be referring to a “single process deployment”, i.e.; a system in which the code is deployed as a single process. There might be multiple instances of this process for scale, resilience etc. But conceptually the entire code is packed and deployed as a single process at a time.

Generally, we think of modularity only from code perspective. However, having modular code is just half the job done. The underlying persistence must also be made modular. So, when you structure your monolith into modules, also have a decomposed DB. Try hard not to have that foreign key reference to another module’s table even if it seems the most sane thing to do.

It might seem an overkill in the beginning to have so many DBs, and it makes sense to have this apprehension. But you can even harness the power of schemas that mostly all the DBs now provide and have separate schemas for different modules.

Modular Monolith with decomposed modular Databases

Segregating the code into microservices, still, is relatively easier as compared to separating the data into individual databases (owned by respective microservices) later and dealing with tedious migrations. DB migration is a “hard problem”.

Feature based packaging:

Firstly, if you’re not familiar with software packaging principles, it’s a good place to begin with and understand the underlying concepts behind them. They are as fundamental to writing and structuring code as are SOLID principles.

“Package by layer” is typically what we’re all aware of and most widely used in projects. If say, we’re working on an API service, we’ll try to segregate the packages by layers. So all the code related to routes will go to a route directory, all the code related to services (or adapters) would fall into the infamous services package and then the pattern continues with entities and repositories and so on so forth.

An example of code structure using package-by-layer packaging style

While the most easy to get started with, Package by layer is a maintenance hell, especially as the project grows, more people start contributing and more and more features get developed. Imagine browsing through the entire entities package to find out just the relevant entities that actually require change. Now imagine doing that for all such layers and imagine all the developers and teams doing that, the pain it would cause. In short, discoverability of code is very low.

“Package by Feature”, on the other hand, uses packages to reflect the feature set. It’s an attempt to place all items related to a single feature (and only that feature) into a simple directory/package. This leads to packages with high cohesion and modularity, with minimal to no coupling between the packages. You may still have the layered architecture in the feature package to begin with, as shown below, but a good north star is to adopt a more Domain Driven Design based packaging structure. For, e.g. if “Food delivery App” had “Restaurant Search” & “Delivery” as capabilities, each of them would end up as a feature package containing relevant layers/sub-packages within them as shown below:

Code structure using package-by-feature style of packaging

Each feature team can individually grow and maintain their feature packages with minimal conflict and bending their minds to search for the right code file to change.

From package by layer towards package by feature code structure

Cross-Functional teams: