Java
Comprehensive Guide to Spring Modulith

Comprehensive Guide to Spring Modulith

The most challenging thing in today’s rapid landscape of software development is adaptability and evolution. While the applications grow in their degree of complexity, the monolithic architecture often becomes a bottleneck on innovation and slows down the development cycles. It is here that modularity will come into play and act as a powerful solution for the challenges.

Enter Spring Modulith: a framework that enables developers to create applications in a truly modular manner. By encouraging a clear separation of concerns, Spring Modulith makes maintenance and updates easier, as well as increasing the ability of large development teams to work in true collaboration. Imagine being able to have different teams working on different modules in parallel, reducing friction and speeding up delivery times.

In this blog post, we are going to look at some core concepts of Spring Modulith, discuss some of its pros and cons, and give practical insights into a small example project. Whether you want to refactor a legacy system, build microservices, or simply clean up your application architecture, Spring Modulith provides a path toward more flexibility and greater scalability. Join us as we discover how this innovative framework can change the way you conceptualize software development.

As always – you can find example project on GitHub under this link: https://github.com/szymon-sawicki/modulith-habit-tracker

Advantages and Disadvantages of Spring Modulith

Advantages

  • Modularity enahnced: Because Spring Modulith wants a clear separation of concerns in one application, maintenance and updates of an application are easier compared to when it is following a classical monolithic architecture.
  • Maintainbility: The framework supports well-structured, domain-aligned applications, which are easier to comprehend and manage during their lifetimes.
  • Simplicity:  By lowering the number of deployment units required compared to microservices, Spring Modulith reduces unnecessary overhead and eliminates the complexity involved in inter-service communications
  • Flexibility:  The ability to develop and test modules independently makes it an evolutionary path if one chooses to migrate to microservices13.Cost-Effectiveness: Consolidating functionalities within a single codebase saves one from complex infrastructure setups, hence cost-effective.

Disadvantages

  • Deployment bottleneck:  Spring Modulith requires a deployment unit, and this will be a potential bottleneck in big applications
  • Limited scalability: The framework is limited to vertical scaling since modules are part of a single process and can’t independently be scaled up like in microservices architectures.
  • Added Complexity: Compared to traditional monoliths, Spring Modulith adds several layers of complexity with regard to module management, and careful consideration must be taken to ensure proper decoupling.
  • Experimental Status: As an experimental project, Spring Modulith might not have all the maturity or stability needed for every production environment.

Spring Modulith gives a reasonable balance for developers who want to keep things simple while still providing modularity and flexibility in applications. On the other hand, careful consideration must be made concerning the limitations it gives, especially scalability and deployment.

Best Practices for Developing with Spring Modulith

  • Define Clear Module Boundaries. Clearly define the boundaries of each module, so that specific domain logic is encapsulated. This helps maintain high cohesion within modules and loose coupling between them.
  • Use Domain-Driven Design (DDD) Principles. Align your modules with either business domains or bounded contexts according to Domain-Driven Design principles. It improves the alignment between the technical and business aspects of the application.
  • Manage Dependencies Effectively. Enforce strict dependency management. Avoid cyclic dependencies by ensuring each module depends only on the necessary pieces of other components, thus keeping things clean and easier to maintain.
  • Leverage Spring Integration. Take advantage of the native integration of Spring Modulith with Spring Boot and the other Spring projects and use a modular architecture with familiar tools and practices.
  • Independent Development and Testing. Develop and test modules independently. The process enables developers to do parallel development, iterate faster, and simplify debugging. Execute @ApplicationModuleTest for an integration test of modules in isolation.
  • Establish Communication Protocols. Perform synchronous communication with well-defined APIs between modules; rely on Spring Modulith’s support for asynchronous communication where that is required. This keeps the modules independent but allows them to interact.
  • Verify Module Structure. Run tests regularly that verify your module setup against the rules provided by Spring Modulith. This will keep your architecture clean and find structural issues as early as possible in your development cycle.
  • Generate Documentation Automatically. Use tools provided by Spring Modulith in order to create documentation snippets, such as component diagrams and module descriptions, based on your application’s structure.

Example Project: Technical Information

The example project demonstrates the practical application of Spring Modulith using the following technologies:

  • Java Version: 23
  • Spring Boot Version: 3.4.0
  • Spring Modulith Version: 1.3.0
  • Build Tool: Gradle
  • Additional Libraries: Lombok, MapStruct, Spring Data JPA

Overview

The application supports the creation and management of goals that include, within each one, a list of habits that are crucial to success. Users can then track the execution of these habits across time, gaining insight into their progress. Two possible roles are foreseen in this application: normal users and administrators, each with different access and functionalities.

This application is quite simple, for learning purposes I not introduced many validations, only some time checks in HabitExecutionService.

Project Structure

The project is organized into several modules, each responsible for specific functionalities:

  • Gateway Module: Acts as the entry point for external interactions, exposing API endpoints and invoking methods from other modules (exposed by *ExternalApi interfaces)
  • Core Modules (Goal, Habit, Tracker, User): Each module contains:
    • ExternalApi interface for communication with the Gateway.
    • InternalApi interface for inter-module communication.
    • DTOs for data transfer.

Each service class implements both – internal and external api interfaces with all method defined there.

One of the most interesting features in Spring Modulith is encapsulation. Only classes from root module package are available from other modules. It means that everything in mapper, model, repository and services packages is accesible only in that particular module.

Usage of events in Spring Modulith

Event-driven communications are utilized in the project to manage dependencies and avoid circular references. For example, when a user is deleted, a UserDeleteEvent is published, which the HabitExecutionService listens for in order to delete associated executions.

The project employs events to facilitate decoupled communication between modules:

  • GoalExistsEvent
  • HabitExistsEvent
  • UserDeleteEvent
  • UserExistsEvent

I am aware, that usage of events for existency check is not the best practice according to maintainbility and performance, but I wanted to achieve decoupling in the fast way. I’ll fix this in the next iteration (I plan to introduce and write further post about testing of Spring Modulith) by introducing dedicated module for check like this.

For publishing of the event we are using Spring’s ApplicationEventPublisher in service. Example (from UserService):

For consistency in database, HabitExecutionService is listening for UserDeleteEvent. For this purpose I used @ApplicationModuleListener from Modulith project, usage of this annotation starts automatically new transaction and offers asynchronous communication between modules. Annotation combines this 3 annotantions from Spring – @Async, @Transactional, and @TransactionalEventListener.

Data model

I’ll describe DTO’s which are sent between modules. Inside of module they are mapped to JPA entities with MapStruct library.

GoalDTO

The GoalDTO represents a user’s goal, including its ID, user ID, name, description, and associated habits.

HabitDTO

The HabitDTO represents a habit linked to a goal, featuring its ID, goal ID, user ID, name, description, and priority (LOW, NORMAL, HIGH).

HabitExecutionDTO

The HabitExecutionDTO tracks the execution of a habit, capturing details like duration, comments, and timestamps.

UserDTO

The UserDTO represents a user, including their ID, username, password, user type (USER or ADMIN), and associated goals.

Conclusion

Spring Modulith is a powerful tool for every developer who wants to build modular applications that would be maintainable and scalable. By embracing modularity, it is easy to handle the complexity of large systems, increase team efficiency in parallel, and quickly integrate new features. 

Challenges with Spring Modulith include deployment bottlenecks and a lack of horizontal scalability, unlike microservices. Its simplicity, though, maintainability, and cost-effectiveness make Spring Modulith a priceless tool among developers.

When looking at Spring Modulith, start thinking about how its modular nature can be tailored to exactly fit the needs of your particular project. Be it bringing an old legacy system to modern standards or creating brand-new applications, the thinking behind modularity will have you producing more robust and more adaptable solutions.

We encourage experiments with the example project above and the exploration of possibilities that Spring Modulith can give in your development tasks. Follow the link in this post to the GitHub repository and begin your work with modular applications at your convenience.