Skip to content

Commit b08d256

Browse files
committed
Add example for Spring Modulith
1 parent 3326c4d commit b08d256

39 files changed

+1114
-0
lines changed

modulith/.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/gradlew text eol=lf
2+
*.bat text eol=crlf
3+
*.jar binary

modulith/.gitignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
HELP.md
2+
.gradle
3+
build/
4+
!gradle/wrapper/gradle-wrapper.jar
5+
!**/src/main/**/build/
6+
!**/src/test/**/build/
7+
8+
### STS ###
9+
.apt_generated
10+
.classpath
11+
.factorypath
12+
.project
13+
.settings
14+
.springBeans
15+
.sts4-cache
16+
bin/
17+
!**/src/main/**/bin/
18+
!**/src/test/**/bin/
19+
20+
### IntelliJ IDEA ###
21+
.idea
22+
*.iws
23+
*.iml
24+
*.ipr
25+
out/
26+
!**/src/main/**/out/
27+
!**/src/test/**/out/
28+
29+
### NetBeans ###
30+
/nbproject/private/
31+
/nbbuild/
32+
/dist/
33+
/nbdist/
34+
/.nb-gradle/
35+
36+
### VS Code ###
37+
.vscode/

modulith/README.adoc

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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].

modulith/build.gradle

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '3.4.4'
4+
id 'io.spring.dependency-management' version '1.1.7'
5+
}
6+
7+
group = 'zin.rashidi.boot'
8+
version = '0.0.1-SNAPSHOT'
9+
10+
java {
11+
toolchain {
12+
languageVersion = JavaLanguageVersion.of(21)
13+
}
14+
}
15+
16+
repositories {
17+
mavenCentral()
18+
}
19+
20+
ext {
21+
set('springModulithVersion', "1.3.4")
22+
}
23+
24+
dependencies {
25+
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
26+
implementation 'org.springframework.modulith:spring-modulith-starter-core'
27+
implementation 'org.springframework.modulith:spring-modulith-starter-jdbc'
28+
runtimeOnly 'org.postgresql:postgresql'
29+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
30+
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
31+
testImplementation 'org.springframework.modulith:spring-modulith-starter-test'
32+
testImplementation 'org.testcontainers:junit-jupiter'
33+
testImplementation 'org.testcontainers:postgresql'
34+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
35+
}
36+
37+
dependencyManagement {
38+
imports {
39+
mavenBom "org.springframework.modulith:spring-modulith-bom:${springModulithVersion}"
40+
}
41+
}
42+
43+
tasks.named('test') {
44+
useJUnitPlatform()
45+
}

modulith/docs/all-docs.adoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
== ModulithApplication
2+
plantuml::components.puml[,,format=svg]
3+
4+
== Course
5+
plantuml::module-course.puml[,,format=svg]
6+
include::module-course.adoc[]
7+
8+
== Student
9+
plantuml::module-student.puml[,,format=svg]
10+
include::module-student.adoc[]
11+
12+
== Subscription
13+
plantuml::module-subscription.puml[,,format=svg]
14+
include::module-subscription.adoc[]
15+

modulith/docs/components.puml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@startuml
2+
set separator none
3+
title ModulithApplication
4+
5+
top to bottom direction
6+
7+
!include <C4/C4>
8+
!include <C4/C4_Context>
9+
!include <C4/C4_Component>
10+
11+
Container_Boundary("ModulithApplication.ModulithApplication_boundary", "ModulithApplication", $tags="") {
12+
Component(ModulithApplication.ModulithApplication.Course, "Course", $techn="Module", $descr="", $tags="", $link="")
13+
Component(ModulithApplication.ModulithApplication.Student, "Student", $techn="Module", $descr="", $tags="", $link="")
14+
Component(ModulithApplication.ModulithApplication.Subscription, "Subscription", $techn="Module", $descr="", $tags="", $link="")
15+
}
16+
17+
Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Course, "listens to", $techn="", $tags="", $link="")
18+
Rel(ModulithApplication.ModulithApplication.Subscription, ModulithApplication.ModulithApplication.Student, "listens to", $techn="", $tags="", $link="")
19+
20+
SHOW_LEGEND(true)
21+
@enduml

modulith/docs/module-course.adoc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[%autowidth.stretch, cols="h,a"]
2+
|===
3+
|Base package
4+
|`zin.rashidi.boot.modulith.course`
5+
|Spring components
6+
|_Services_
7+
8+
* `z.r.b.m.c.CourseManagement`
9+
10+
_Repositories_
11+
12+
* `z.r.b.m.c.CourseRepository`
13+
14+
_Event listeners_
15+
16+
* `o.s.c.ApplicationListener`
17+
18+
_Others_
19+
20+
* `z.r.b.m.c.CourseEventsConfiguration`
21+
|Published events
22+
|* `z.r.b.m.c.CourseEnded` created by:
23+
** `z.r.b.m.c.CourseEventsConfiguration.courseEnded(…)`
24+
25+
|Events listened to
26+
|* `org.springframework.context.ApplicationEvent`
27+
|===

0 commit comments

Comments
 (0)