From spring
Structure Spring Boot applications as explicit application modules with boundary verification, published module events, and module-interaction tests via Spring Modulith. Use this skill when structuring a Spring Boot application as explicit application modules, verifying module boundaries, publishing module events, and testing module interactions with Spring Modulith.
npx claudepluginhub ririnto/sinon --plugin springThis skill uses the workspace's default tool permissions.
Use this skill when structuring a Spring Boot application as explicit application modules, verifying module boundaries, publishing module events, and testing module interactions with Spring Modulith.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Use this skill when structuring a Spring Boot application as explicit application modules, verifying module boundaries, publishing module events, and testing module interactions with Spring Modulith.
Use spring-modulith for package-level application module boundaries, named interfaces, module verification, module events, and module-focused tests.
The ordinary Spring Modulith job is:
| Situation | Use |
|---|---|
| Other modules may use only a subset of types | named interface |
| Cross-module reaction should happen after work completes | application event |
| Boundary drift must fail fast in CI | ApplicationModules.verify() |
| One module interaction needs isolated integration coverage | @ApplicationModuleTest |
Keep named interfaces small and intention-revealing. Prefer events over direct internal bean calls when the interaction does not need immediate synchronous coupling.
When one module may depend on only specific neighbors, make that dependency rule explicit rather than relying on package conventions alone.
Import the BOM and use the core and test starters for the common path.
The current stable Spring Modulith line is 2.0.5. The 2.1.x line is still milestone-only and should be treated as upcoming until it reaches GA.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>2.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
./mvnw test -Dtest=ModularityTests
./gradlew test --tests ModularityTests
ApplicationModules.of(Application.class).verify();
events.publishEvent(new OrderCompleted(order.id()));
@Component
class InventoryProjection {
@ApplicationModuleListener
void on(OrderCompleted event) {
}
}
Use @ApplicationModuleListener when the listener should clearly belong to the cross-module event boundary.
@ApplicationModule
package example.orders;
@NamedInterface("api")
package example.orders.api;
Use these package-info.java declarations when the module boundary and the exported named interface should be explicit in code rather than implied only by package names.
Start by making verification pass before adding richer module test scenarios.
@ApplicationModuleTest only after ordinary boundary verification is already clean.| Situation | Use |
|---|---|
| One module should start with only its direct collaborators | @ApplicationModuleTest(mode = STANDALONE) |
| Test should see direct dependency modules too | @ApplicationModuleTest(mode = DIRECT_DEPENDENCIES) |
| Event publication must be asserted directly | PublishedEvents or AssertablePublishedEvents |
| Efferent dependency should stay mocked in the module test | @MockitoBean |
class ModularityTests {
@Test
void verifiesModuleBoundaries() {
ApplicationModules.of(Application.class).verify();
}
}
@Service
class Orders {
private final ApplicationEventPublisher events;
Orders(ApplicationEventPublisher events) {
this.events = events;
}
void complete(Order order) {
order.complete();
events.publishEvent(new OrderCompleted(order.id()));
}
}
@Component
class InventoryProjection {
private final InventoryReadModel readModel;
InventoryProjection(InventoryReadModel readModel) {
this.readModel = readModel;
}
@ApplicationModuleListener
void on(OrderCompleted event) {
readModel.markCompleted(event.orderId());
}
}
@ApplicationModuleTest(mode = ApplicationModuleTest.BootstrapMode.STANDALONE)
class OrdersModuleTest {
@Autowired
Orders orders;
@Test
void completesOrderAndPublishesModuleEvent(PublishedEvents publishedEvents) {
orders.complete(new Order("o-1"));
assertThat(publishedEvents.ofType(OrderCompleted.class)).hasSize(1);
}
}
@ApplicationModuleTest
class OrdersModuleTest {
@Autowired
Orders orders;
@Test
void publishesOrderCompleted(PublishedEvents events) {
orders.complete(new Order("o-1"));
assertThat(events.ofType(OrderCompleted.class)).hasSize(1);
}
}
example.orders.api
example.orders.internal
@ApplicationModule(allowedDependencies = "inventory::api")
package example.orders;
ApplicationModules.of(Application.class).verify();
new OrderCompleted(order.id())
@ApplicationModuleTest
class OrdersModuleTest {
}
allowedDependencies or several named interface packages.Scenario verification than the ordinary module test path.