| Moduliths | Microservices | |
|---|---|---|
Domain structure |
|
|
Application meta-work focus |
Clearly separating the bounded contexts, prevent accidental coupling. |
Managing infrastructure concerns and challenges induced by the fact that we’re building a distributed system. |
[plus circle] — In the best case, even breaking API changes can be propagated through the system immediately as the compiler builds a huge part of the system ad hoc. |
[minus circle] — As API provider and consumer are distant, special means of verification need to be deployed to detect breaking changes (e.g. consumer-driven contracts). |
|
Means of implementing modularity |
Programming language specific means of encapsulation. In Java: classes, packages, artifacts (i.e. build system modules). |
Deployment units and the network APIs they expose. |
API abstraction level |
Platform level APIs (e.g. Java). Modules expose aggregates, Spring components and publishes as well as consumed events. |
HTTP / Messaging. Modules expose resources and API definition via the schema of the representations (usually JSON documents) they exchange. |
Module interaction model |
Direct method invocations and application events within the same process, which makes them fast and less susceptible to error conditions. The method call can either succeed or throw an exception. |
Network calls to other systems via HTTP, any RPC technology or messaging via a broker. That induces the need for serialization and deserialization of data as well as guards against communication problems on the network like circuit breakers, retries etc. Also, that interaction is significantly slower than direct method invocations and implies the need to embrace and deal with eventual consistency. |
Consistency |
Strong consistency preferred, although eventual consistency can be embraced t the degree needed (ideally cross aggregates). Transactions can be used by the book, e.g. to apply changes on a single aggregate but also span multiple ones if needed and the consequences are acceptable. The latter should be a conscious decision and made explicit. The ability to "veto" a unit of work by foreign modules (e.g. through an event listener participating in the same transaction) provides a certain level of convenience but also increases coupling. |
Eventual consistency must be embraced, adding complexity. Business transactions that include multiple modules need to use sagas and compensating actions. |
Integration |
Focused on infrastructure components that are owned by the application. Integration with 3rd-party systems is rare(r) |
Interacting with other systems is a primary concern of the architecture and needs development focus. Designing the system to avoid interaction with other systems while serving user requests needs careful design. |
Means of verifying dependencies |
Build system modules. Static code analysis tools like jDepend, Structure 101, Moduliths. |
Runtime communication analysis through Zipkin etc. |
Freedom of technology choice |
Limited to the chosen platform. In the case of the JVM, multiple JVM languages can be combined theoretically but significantly increase build complexity. |
Given. As the abstraction level of interactions |
Source code organization |
Likely a single repository, structured into build system modules as needed on the spectrum between single-module, packages-only to build module per logical module. Constraints:
|
Separate repositories per bounded context to be able to individually release and deploy versions of the involved systems. |
Developer productivity |
The codebase can usually be imported into the IDE as one or as all build modules individually. |
The design of business functionality within a single module is usually simpler than in a monolithic arrangement. The additional technical challenges usually add complexity in that regard. Other systems and infrastructure need to be set up to run the module individually and application as a whole. |
Build & Deployment |
Usually as a whole. Being able to run a build considering changes of individual modules only requires tweaks to the build setup and CI infrastructure. |
Usually individually. Building a single module is the default. Requires verification of compatibility though (see [refactoring]). |
Testing |
Unit and integration tests. The latter usually run the entire application. Horizontal slices testable via Spring Boot support. Horizontal slices via Moduliths. Testing the system entirely is the default model in integration tests. Challenges:
|
Integration tests run on the individual module level by default. Challenges:
|
Scalability |
[minus circle] — Individual modules cannot be scaled separately easily. One, limited means to help here is caching which can be applied more aggressively to some parts of the system but the effect that this delivers is limited by the ability to cache that part of the domain in the first place. |
[plus circle] — As the individual modules are deployed separately, each of them can be scaled individually. |
Isolation |
[minus circle] — Not given by default and only achievable on the dependency level by using technologies like OSGi that provide class loader level isolation of code. Memory and CPU are shared. |
[plus circle] — Resources can be assigned by module. |
Last active
June 22, 2025 10:09
-
-
Save odrotbohm/b9d77bf02f0072b7142e6ad3b9bd63f7 to your computer and use it in GitHub Desktop.
Modulith & Microservice – A trade-off comparison
Regarding the Source code organization, some large-scale organizations deploy microservices as independent units, but keep the code of all of them in a monorepo to mitigate some of your points.
Some (lots?) of the bullet points in the second column conflate deployment units and multi-repos. A more realistic view would be to split it in 2: monorepos and multi-repos.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great comparison!
I don't know if this is relevant and correct, but I think with microservices it is often the case that each microservice has its own database (and probably only one in most cases).
That's why you don't have to worry within the code about which part of the code works with which database (because there's only one). In Modulith you might have only one database for all modules (all examples I have seen so far use only one database). If you wanted to use one DB per module, it doesn't feel so ‘typical’ and simple in Spring in my opinion, because all the default configurations etc. are designed for one database in the application. However, a DB per module would make it easier to extract a module into a separate service later on. And even if you would choose one DB for the whole modulith application (for consistency reasons for example), it's different than with microservices.
Debugging might also be easier within a modulith (using the IDE's built-in debugger) than accross single microservices