This document contains a tabular comparison of the Modulithic and Microservice Architecture approach under certain aspects. Many of them can be achieved or influenced in both styles, but to different degrees and using different means. No claims of exhaustiveness.
Modulith | 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. |
Ability to refactor # |
[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. Refactorings guided by IDEs. |
[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). For Spring applications: Spring Modulith. |
Deployment units and the network APIs they expose. |
API abstraction level # |
Platform level APIs (e.g. Java). Modules expose domain types or DTOs, Spring components, and published as well as consumed events. |
HTTP / Messaging. Modules expose resources and API definitions via the schema of the representations (usually JSON documents) they exchange. Also: GraphQL. |
Module interaction model # |
Direct method invocations and (asynchronous) 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. Eventual consistency optional (see Consistency). |
Network calls to other systems via HTTP, any RPC technology or messaging via a broker. That introduces 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) and is supported through Spring Modulith’s Event Publication Registry. 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. ArchUnit, jQAssistant. Static code analysis tools like jDepend, Structure 101, Spring Modulith. |
Runtime communication analysis through Zipkin etc. |
Freedom of technology choice # |
[plus circle] — Limited to the chosen platform. In the case of the JVM, multiple JVM languages can be combined theoretically but significantly increase build complexity. |
[plus circle] — Full freedom of choice of technology as the means of operation are individual operating system processes. |
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 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. Vertical slices via Spring Modulith (integration test documentation). Testing the system entirely is the default model in integration tests. Version control system inspection can help detecting which tests need to be run (Spring Modulith test optimization documentation). 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 individual process. |
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