|
| 1 | += Spring Modulith: Building Modular Monolithic Applications |
| 2 | + |
| 3 | +== Introduction |
| 4 | + |
| 5 | +This tutorial demonstrates how to use Spring Modulith to build a modular monolithic application. Spring Modulith is a framework that helps structure applications into well-defined modules with clear boundaries while still deploying as a single unit. |
| 6 | + |
| 7 | +In this example, we've built a simple student course management system with three modules: |
| 8 | + |
| 9 | +* *Course*: Manages course information and lifecycle |
| 10 | +* *Student*: Manages student information and status |
| 11 | +* *Subscription*: Manages the relationship between students and courses |
| 12 | + |
| 13 | +== What is Spring Modulith? |
| 14 | + |
| 15 | +Spring Modulith is an extension to the Spring ecosystem that provides: |
| 16 | + |
| 17 | +* Clear module boundaries through package conventions |
| 18 | +* Explicit module dependencies |
| 19 | +* Event-based communication between modules |
| 20 | +* Testing support for modules in isolation |
| 21 | +* Documentation generation for module structure |
| 22 | + |
| 23 | +== Project Structure |
| 24 | + |
| 25 | +The project follows the Spring Modulith package convention: |
| 26 | + |
| 27 | +[source] |
| 28 | +---- |
| 29 | +zin.rashidi.boot.modulith |
| 30 | +├── ModulithApplication.java |
| 31 | +├── course |
| 32 | +│ ├── Course.java |
| 33 | +│ ├── CourseEnded.java |
| 34 | +│ ├── CourseEventsConfiguration.java |
| 35 | +│ ├── CourseManagement.java |
| 36 | +│ └── CourseRepository.java |
| 37 | +├── student |
| 38 | +│ ├── Student.java |
| 39 | +│ ├── StudentEventsConfiguration.java |
| 40 | +│ ├── StudentInactivated.java |
| 41 | +│ ├── StudentManagement.java |
| 42 | +│ └── StudentRepository.java |
| 43 | +└── subscription |
| 44 | + ├── Subscription.java |
| 45 | + ├── SubscriptionManagement.java |
| 46 | + └── SubscriptionRepository.java |
| 47 | +---- |
| 48 | + |
| 49 | +Each module is a separate package under the application's base package. |
| 50 | + |
| 51 | +== Module Interactions |
| 52 | + |
| 53 | +The modules interact with each other through events: |
| 54 | + |
| 55 | +1. When a course ends, the Course module publishes a `CourseEnded` event |
| 56 | +2. When a student is inactivated, the Student module publishes a `StudentInactivated` event |
| 57 | +3. The Subscription module listens for these events and cancels the relevant subscriptions |
| 58 | + |
| 59 | +This event-based communication ensures loose coupling between modules. |
| 60 | + |
| 61 | +== Key Components |
| 62 | + |
| 63 | +=== Domain Entities |
| 64 | + |
| 65 | +Each module has its own domain entity: |
| 66 | + |
| 67 | +* `Course`: Represents a course with a name and status (ACTIVE, DORMANT, ENDED) |
| 68 | +* `Student`: Represents a student with a name and status (ACTIVE, INACTIVE) |
| 69 | +* `Subscription`: Represents a relationship between a student and a course with a status (ACTIVE, COMPLETED, DORMANT, CANCELLED) |
| 70 | + |
| 71 | +=== Repositories |
| 72 | + |
| 73 | +Each module has its own repository for data access: |
| 74 | + |
| 75 | +* `CourseRepository`: Basic CRUD operations for courses |
| 76 | +* `StudentRepository`: Basic CRUD operations for students |
| 77 | +* `SubscriptionRepository`: CRUD operations plus custom methods for cancelling subscriptions by course or student |
| 78 | + |
| 79 | +=== Services |
| 80 | + |
| 81 | +Each module has a service class for business logic: |
| 82 | + |
| 83 | +* `CourseManagement`: Updates course information |
| 84 | +* `StudentManagement`: Manages student status |
| 85 | +* `SubscriptionManagement`: Listens for events and manages subscriptions accordingly |
| 86 | + |
| 87 | +=== Events |
| 88 | + |
| 89 | +The application uses domain events for communication between modules: |
| 90 | + |
| 91 | +* `CourseEnded`: Published when a course status is set to ENDED |
| 92 | +* `StudentInactivated`: Published when a student status is set to INACTIVE |
| 93 | + |
| 94 | +== Testing |
| 95 | + |
| 96 | +Spring Modulith provides excellent testing support: |
| 97 | + |
| 98 | +* `ModuleTests`: Verifies the modulith architecture and generates documentation |
| 99 | +* Module-specific tests: Test each module in isolation or with its dependencies |
| 100 | + |
| 101 | +=== Architecture Verification |
| 102 | + |
| 103 | +The `ModuleTests` class includes a test that verifies the modulith architecture: |
| 104 | + |
| 105 | +[source,java] |
| 106 | +---- |
| 107 | +@Test |
| 108 | +@DisplayName("Verify architecture") |
| 109 | +void verify() { |
| 110 | + modules.verify(); |
| 111 | +} |
| 112 | +---- |
| 113 | + |
| 114 | +This test ensures that module dependencies are correctly defined and that there are no unwanted dependencies between modules. |
| 115 | + |
| 116 | +=== Documentation Generation |
| 117 | + |
| 118 | +The `ModuleTests` class also includes a test that generates documentation: |
| 119 | + |
| 120 | +[source,java] |
| 121 | +---- |
| 122 | +@Test |
| 123 | +@DisplayName("Generate documentation") |
| 124 | +void document() { |
| 125 | + new Documenter(modules, defaults().withOutputFolder("docs")) |
| 126 | + .writeModulesAsPlantUml() |
| 127 | + .writeDocumentation(Documenter.DiagramOptions.defaults(), Documenter.CanvasOptions.defaults().revealInternals()); |
| 128 | +} |
| 129 | +---- |
| 130 | + |
| 131 | +This test generates documentation in the `docs` folder, including PlantUML diagrams and AsciiDoc files for each module. |
| 132 | + |
| 133 | +=== Testing Event-Based Communication |
| 134 | + |
| 135 | +Spring Modulith provides excellent support for testing event-based communication between modules. Here are examples from our test classes: |
| 136 | + |
| 137 | +==== Publishing Events |
| 138 | + |
| 139 | +The `CourseManagementTests` class demonstrates how to test event publishing: |
| 140 | + |
| 141 | +[source,java] |
| 142 | +---- |
| 143 | +@ApplicationModuleTest |
| 144 | +class CourseManagementTests { |
| 145 | +
|
| 146 | + @Autowired |
| 147 | + private CourseManagement courses; |
| 148 | +
|
| 149 | + @Test |
| 150 | + @DisplayName("When a course is ENDED Then CourseEnded event will be triggered with the course Id") |
| 151 | + void courseEnded(Scenario scenario) { |
| 152 | + var course = new Course("Advanced Java Programming").status(ENDED); |
| 153 | + ReflectionTestUtils.setField(course, "id", 2L); |
| 154 | +
|
| 155 | + scenario.stimulate(() -> courses.updateCourse(course)) |
| 156 | + .andWaitAtMost(ofMillis(101)) |
| 157 | + .andWaitForEventOfType(CourseEnded.class) |
| 158 | + .toArriveAndVerify(event -> assertThat(event).extracting("id").isEqualTo(2L)); |
| 159 | + } |
| 160 | +} |
| 161 | +---- |
| 162 | + |
| 163 | +.This test: |
| 164 | +. Uses `@ApplicationModuleTest` to test the Course module |
| 165 | +. Uses `Scenario.stimulate()` to trigger an action (updating a course) |
| 166 | +. Uses `andWaitAtMost()` to specify a maximum wait time |
| 167 | +. Uses `andWaitForEventOfType()` to wait for a specific event type |
| 168 | +. Uses `toArriveAndVerify()` to verify the event's properties |
| 169 | + |
| 170 | +Similarly, the `StudentManagementTests` class tests event publishing from the Student module: |
| 171 | + |
| 172 | +[source,java] |
| 173 | +---- |
| 174 | +@ApplicationModuleTest |
| 175 | +class StudentManagementTests { |
| 176 | +
|
| 177 | + @Autowired |
| 178 | + private StudentManagement students; |
| 179 | +
|
| 180 | + @Test |
| 181 | + @DisplayName("When the student with id 4 is inactivated Then StudentInactivated event will be triggered with student id 4") |
| 182 | + void inactive(Scenario scenario) { |
| 183 | + var student = new Student("Bob Johnson"); |
| 184 | + ReflectionTestUtils.setField(student, "id", 4L); |
| 185 | +
|
| 186 | + scenario.stimulate(() -> students.inactive(student)) |
| 187 | + .andWaitForEventOfType(StudentInactivated.class) |
| 188 | + .toArriveAndVerify(inActivatedStudent -> assertThat(inActivatedStudent).extracting("id").isEqualTo(4L)); |
| 189 | + } |
| 190 | +} |
| 191 | +---- |
| 192 | + |
| 193 | +==== Consuming Events |
| 194 | + |
| 195 | +The `SubscriptionManagementTests` class demonstrates how to test event consumption: |
| 196 | + |
| 197 | +[source,java] |
| 198 | +---- |
| 199 | +@ApplicationModuleTest |
| 200 | +class SubscriptionManagementTests { |
| 201 | +
|
| 202 | + @Autowired |
| 203 | + private SubscriptionRepository subscriptions; |
| 204 | +
|
| 205 | + @Test |
| 206 | + @DisplayName("When CourseEnded is triggered with id 5 Then all subscriptions for the course will be CANCELLED") |
| 207 | + void courseEnded(Scenario scenario) { |
| 208 | + var event = new CourseEnded(5L); |
| 209 | +
|
| 210 | + scenario.publish(event) |
| 211 | + .andWaitForStateChange(() -> subscriptions.cancelByCourseId(5L)) |
| 212 | + .andVerify(updatedRows -> assertThat(updatedRows).isEqualTo(2)); |
| 213 | + } |
| 214 | +
|
| 215 | + @Test |
| 216 | + @DisplayName("When StudentInactivated is triggered with id 5 Then all subscriptions for the student will be CANCELLED") |
| 217 | + void studentInactivated(Scenario scenario) { |
| 218 | + var event = new StudentInactivated(5L); |
| 219 | +
|
| 220 | + scenario.publish(event) |
| 221 | + .andWaitForStateChange(() -> subscriptions.cancelByStudentId(5L)) |
| 222 | + .andVerify(updatedRows -> assertThat(updatedRows).isEqualTo(2)); |
| 223 | + } |
| 224 | +} |
| 225 | +---- |
| 226 | + |
| 227 | +.This test: |
| 228 | +. Uses `@ApplicationModuleTest` to test the Subscription module |
| 229 | +. Uses `Scenario.publish()` to publish an event |
| 230 | +. Uses `andWaitForStateChange()` to wait for a state change in the system |
| 231 | +. Uses `andVerify()` to verify the result of the state change |
| 232 | + |
| 233 | +== Generated Documentation |
| 234 | + |
| 235 | +Spring Modulith automatically generates documentation for your modules. You can view the generated documentation in the link:docs/all-docs.adoc[docs/all-docs.adoc] file. |
| 236 | + |
| 237 | +== Conclusion |
| 238 | + |
| 239 | +Spring Modulith provides a powerful way to structure your Spring Boot applications into well-defined modules while still deploying as a single unit. By following package conventions and using event-based communication, you can build modular monolithic applications that are easier to understand, test, and maintain. |
| 240 | + |
| 241 | +For more information, visit the https://spring.io/projects/spring-modulith[Spring Modulith website]. |
0 commit comments