You are an engineering manager and your teams work with a (modular) monolithic code base – please ensure:
There is no common definition of Monolith, so I refer to mine in the parts below:
a single software system based on a single codebase with multiple functionality.
With the rise of microservices architecture, software engineers realized apart from the upsides also downsides. These downsides include amongst others additional complexity and distribution.
When the software domain is not well understood or changing, a monolithic architecture can be beneficial because of its reduced initial complexity and distribution (compared with complexity and distribution in a microservice architecture).
Monolithic software architectures have advantages when rapid development and changing gears is necessary because the scope of the application is being explored while development is ongoing.
Monolith to Microservices
Breaking a monolith architecture into microservices architecture can be a reasonable move at a certain moment in development of a software in particular when multiple (autonomous) teams work on different parts of the monolith. Dividing a monolith into smaller parts/(micro)services is beneficial when
- the advantages (ideally measurable in money) of independent micro services development of autonomous teams
- the costs (ideally measurable in money) of continuing working with the monolith.
Microservices refers in this context to a software architecture that consists of independent software services, each based on a separate code base.
Are the two options
really the only options?
The modular monolith approach aims to combine the best of both. In my opinion a modular monolith is:
a single software system based on a single codebase that is organized by a set of independent, interchangeable modules.
These modules can be of different sizes and granularity, it is important that the modules are as loosely coupled as possible to be independent and interchangeable.
Interfaces are key to loose coupling. These interfaces must be clear and well documented. The technology (GRPC, REST or other) must fit as well, however the clarity is key.
Modularization – software development focused on independent and interchangeable modules is in my experience a general good advice to increase reusability, maintainability, and scalability to name a few. Whether your teams work with a modular monolith or “just” a monolith or even with microservices is less important than striving for modularization.
Modules’ code separation shall ensure independent parts of a code base.
In (modular) monolith code bases I observed the facade pattern to achieve this, sometimes implemented with REST, GRPC, GraphQL APIs and similar technologies.
Even when code is separated (more or less) using the facade pattern and I asked to consider the following cases:
Ensure your teams have enough capacity to review code contributions from other teams (intern or extern). Otherwise you risk code contribution proposals (e.g. as pull requests) are not reviewed thoroughly enough and may soften the (code) separation.
Another aspect that softens (code) separation in my experience is shared libraries. Apart from the non trivial decision of when and what code functionality to outsource into shared libraries, these shared libraries sooner or later will exist in different versions. With that, the subject of backwards compatible changes vs breaking changes is important.
In retrospect I would have paid more attention to outdated shared libraries’ versions in monolith’s modules and subsequently different library versions. In theory those versions were backwards compatible. However the functionality changed significantly over time that the perception was “not really backwards compatible”.
Data store separation
Most of the modules I experienced contained data that needs to be stored in some kind of datastore (e.g. a database). There is a spectrum between a
- shared datastore (no separation between modules’ data)
- specific and separated datastore per module.
One option between these extremes is data separation on data bucket level (still same data store) respectively database table/schema level (still same database).
Competing database writes
I remember a situation in which database writes competed for the same records in a database table. This resulted in race conditions that outcomes were not deterministic. Subsequently we did not know which write operation did win per data item. There are multiple options to resolve this, however one of the contribution factors was the missing data separation. In retrospect I consider this a flaw in the software architecture.
Facade like data separation
In retrospect I should have paid more attention to separation on the database table level. I remember failed data migrations resulting in an undefined state of data – the migrations progress and subsequently the state of the data was not known when the migration failed. The fix required at least weeks of effort. If data migrations were separated on database table level in every step of the data migration such a fix would be shorter.
In similar future cases, I’d consider append-only tables (or databases). This way, all steps of data migrations are immutable and can be referenced for reconciliation if the migrations fail.
- https://en.wikipedia.org/wiki/Facade_pattern as part of https://en.wikipedia.org/wiki/Design_Patterns