Skip to content

Instantly share code, notes, and snippets.

@odrotbohm
Last active June 22, 2025 10:09
Show Gist options
  • Save odrotbohm/b9d77bf02f0072b7142e6ad3b9bd63f7 to your computer and use it in GitHub Desktop.
Save odrotbohm/b9d77bf02f0072b7142e6ad3b9bd63f7 to your computer and use it in GitHub Desktop.
Modulith & Microservice – A trade-off comparison

Modulith VS. Microservices

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 #

  • Complex

  • Explorative, Bounded Context boundaries likely to shift

  • Simple

  • Well-known

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:

  • The need to consume and assemble different sets of logical modules into the final runtime.

  • The question of whether logical dependency management should be applied via build tool dependency management.

  • The overhead induced by the split up into multiple build modules.

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:

  • Running integration tests for individual modules. Spring Modulith’s integration tests features help here.

Integration tests run on the individual module level by default.

Challenges:

  • Unless explicitly designed to avoid this, testing is likely to require other modules and some infrastructure to run as well, complicating the undertaking.

  • Testing the overall system requires the individual modules to be run. The definitions of the overall system tests also have to live somewhere.

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.

@nilshartmann
Copy link

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

@nfrankel
Copy link

nfrankel commented Jun 5, 2025

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