A modular monolith architecture

A modular monolith architecture
Photo by Simon Goetz on Unsplash

The recent announcement of Spring Monolith (Introducing Spring Modulith) fits well into the rising interest in monolithic architectures (Monolithic Architecture Trend). It seems like not everything has to become its own microservice.

That’s why some time ago I was thinking about which learnings from a microservice architecture could be applied to a monolithic architecture. I came up with a sample project called “modular_monolith” and the resulting source code can be checked out on my Github account (modular_monolith). In this post I want to give an overview over my thought process and how & why I came up with this architecture.

Disclaimer: This architecture is a thought experiment, albeit as close as possible to real life. Your concrete use case might not match the use case presented here. If you intend to use this architecture, make sure to adjust, simplify or ignore certain aspects.

The requirements

To get a realistic example, I came up with the following (business) requirements for the architecture:

  • There are at least two independent teams working on the same codebase. They have independent meetings, plannings, dailies, etc.
  • The “app” team owns the repository. The also own the internal indexation and external app deployable
  • The “ai” team contributes to these deployables). This would include calling their microservice(s) as well as adding code to the presented repository. The code from the “app” team and the “ai” team needs to call each other
  • Any failure in the code of the “ai” team should not lead to downtime for the modules depending on it
  • The “app” deployable needs to be highly scalable, going from 10 up to 200 deployments (e.g. pods)
  • The “ai” services do not linearly scale to the “app” deployments
  • Dependencies need to be as hidden as possible, but the deployable should set the version (e.g. Spring Boot) for modules it depends on
  • External service failures need to be gracefully handled

The rules

I’ve formulated some rules to accustom these requirements:

  • Every outgoing connection needs to be wrapped in circuit breakers and fallbacks. Teams agree on SLAs and fallbacks
  • Every outgoing call needs to go through some defined adapters. This concept is from the ports and adapters architecture (Hexagonal Architecture)
  • Every module defines its own dependencies and configs and keeps them as encapsulated as possible
  • Every module has a clear owner. Other teams can participate, but their code needs to be isolated
  • Every other team is treated as foreign code and service, as if it was another microservice
  • Dependencies in a module should never be transitively exposed to other modules
  • View dependencies as part of your responsibility and think about them actively
  • Classes should be package private by default. One could e.g. cut the packages by domain or by feature
  • No common modules as implied by the rules above. Common modules breed transitive dependencies and have no clear ownership

Concrete implementation

To hide dependencies between modules we’ll make use of the isolation mechanism from Gradle (Gradle Implementation Keyword). Specifying dependencies via implementation will not expose transitive dependencies via the classpath (only during runtime) to modules which depend on them. With gradle we can also define common plugins to define the versions for depending modules. Unfortunately, I was not able to achieve the requirement of having only the deployable modules dictate the dependencies. Currently, the Spring Boot version is defined for the whole monolith.

For isolating code, resilience4j (Resilience4j) is quite powerful and easy to use. The only drawback (at the time of writing) is that it does not support a distributed state of a breaker. With this the owners can wrap foreign code in safe calls and define fallbacks.

Find the code here: modular_monolith

High-level overview

  • The “app” team owns the repository as well as the internal (indexation) and external (app) deployable
  • The “ai” team contributes to these deployables by using certain ai features (ai). This would include calling their microservice(s) not in this repository as well as adding business logic
  • The internal deployed application follows an event-driven approach and shares the persisted entities (entities) with the user-facing services for convenience
  • The external deployed service follows a classic non-reactive layered architecture approach

Dependency definitions

Keeping it simple

Concerning the module entities, if I stuck to a strict and correct isolation, I would have to duplicate the entities in both modules (indexation) and (app). While this is certainly the more correct way, I decided to go for a more pragmatic approach. Depending on the complexity of the app, this might or might not be acceptable.

The drawbacks

  1. Even if we encapsulate all dependencies and modules as much as possible and wrap them in circuit breakers, we can’t avoid all dependencies. If module B introduces a dependency to a database, the deployable module A, depending on module B also depends on that database now. As example, if your deployable scales very high, you might exhaust the maximum connections of the database even though the database might not even be used that much by module B.

  2. We still cannot bump the version of Spring Boot for one deployable, but keep an older version in another deployable

References

  1. Introducing Spring Modulith. https://spring.io/blog/2022/10/21/introducing-spring-modulith. Accessed 08.12.2022.
  2. Monolithic Architecture Trend. https://trends.google.de/trends/explore?date=2017-11-09%202022-12-08&q=monolithic%20architecture. Accessed 08.12.2022.
  3. modular_monolith. https://github.com/agraphie/modular_monolith. Accessed 08.12.2022.
  4. Hexagonal Architecture. https://alistair.cockburn.us/hexagonal-architecture/. Accessed 08.12.2022.
  5. Gradle Implementation Keyword. https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_separation. Accessed 08.12.2022.
  6. Resilience4j. https://resilience4j.readme.io/. Accessed 08.12.2022.