Enforce module boundaries at test time.
Modulith Rules is a set of pre-built ArchUnit rules for modular monoliths. It runs in your test phase, so nothing is added to the production classpath and the core module has no framework dependency.
Rules run during mvn test. Nothing ships to production, and the core module has no Spring dependency.
Boundaries, cycles and communication contracts in the core module; controllers, repositories, transactions, events and injection in the Spring module.
Declare modules in Java, in modulith-rules.yml, or let each base package be derived as <root>.<module>.
Add the dependency
Core module, test scope. Add modulith-rules-spring for the Spring rules, or import the BOM to align versions.
<dependency>
<groupId>io.modularmonolith</groupId>
<artifactId>modulith-rules-core</artifactId>
<version>0.2.0</version>
<scope>test</scope>
</dependency>
Write one test
The convention-based path: each module's base package is derived from the root package and the module name.
@AnalyzeClasses(packages = "com.example")
class ArchitectureTest {
@ArchTest
static final List<ArchRule> rules =
ModulithRules.forPackage("com.example",
"ordering", "payments", "inventory")
.allRules();
}
What this test checks
allRules() applies three checks: allowed dependencies, internal-package protection and cycle detection.
An empty allowedDependencies setting leaves a module’s dependencies unrestricted.
API-only access requires configured API packages, and is checked by allCoreRules().
Classes outside registered modules are ignored.
A failing build, then the fix
An ordering class reaches into payments.internal. The test above fails the build; routing the call through payments.api makes it pass.
package com.example.ordering.internal;
import com.example.payments.internal.PaymentServiceImpl;
class OrderServiceImpl {
private final PaymentServiceImpl payments;
// ...
}
@ArchTest
static final List<ArchRule> rules =
ModulithRules.forPackage("com.example",
"ordering", "payments", "inventory")
.allRules();
Class com.example.ordering.internal.OrderServiceImpl in module 'ordering' accesses com.example.payments.internal.PaymentServiceImpl which is in the internal package of module 'payments'. Fix: expose a service interface in the module's api package, for example com.example.payments.api, or move com.example.ordering.internal.OrderServiceImpl into module 'payments' if it belongs there
package com.example.ordering.internal;
import com.example.payments.api.PaymentService;
class OrderServiceImpl {
private final PaymentService payments;
// ...
}
The rule catalogue
See the violation messages →allRules() returns three rules: allowed dependencies, internal protection, cycle detection. allCoreRules() adds API-only access and communication contracts. Both of the added rules skip modules that have not declared the relevant configuration, so the longer list is safe to start with.
Boundary rules
modulesOnlyDependOnAllowedModules()internalsShouldNotBeAccessedFromOutside()crossModuleAccessOnlyThroughApi()moduleShouldHaveNoDependencies(name)moduleShouldOnlyBeAccessedBy(name, ...)onlyAllowedDependenciesAreUsed()noModuleAccessesInternalsOfOthers()Cycle rules
noModuleCycles()moduleHasNoCycles(name)noCircularDependenciesBetweenModules()noModuleCycles()CycleRules.detectCycles(graph) is public and static, so you can run the DFS against a hand-built graph without importing any classes.Communication rules
asyncModulesShouldNotCallDirectly()noCommModulesShouldNotInteract()allCommunicationContractsRespected()Spring rules
modulith-rules-springcontrollersShouldNotCrossModuleBoundaries()repositoriesShouldBeModuleInternal()transactionalMethodsShouldNotSpanModules()eventClassesShouldBeInApiPackages()noDirectInjectionOfInternalBeans()Export the module graph
dependencyGraph().toMermaid(classes) renders the module graph as a Mermaid flowchart string, ready to paste into any Markdown file. Every registered module appears, including isolated ones.
Solid arrows are observed dependencies; an arrow renders dashed when the source module declares an ASYNCHRONOUS contract. Dashed arrows show declared contracts, not verified runtime messaging. Nodes sort by name and edges by source then target, so committing the diagram produces a clean diff.
flowchart LR
inventory
notifications
ordering
payments
notifications -.-> ordering
ordering --> inventory
How it differs from Spring Modulith
Modulith Rules is a focused architecture-testing library built on ArchUnit. Spring Modulith is a broader toolkit for building modular Spring Boot applications. Both support architecture verification during tests.
Spring Modulith already provides built-in checks for module cycles, access to internal packages, and explicitly allowed dependencies through ApplicationModules.verify(). It also provides module integration testing, generated documentation, and optional runtime features such as event publication infrastructure and observability.
Modulith Rules focuses on configurable architecture policies: module boundaries, dependency cycles, directional communication contracts, and opinionated Spring-specific checks. Its core module has no Spring dependency.
| Concern | Modulith Rules | Spring Modulith |
|---|---|---|
| Primary focus | Configurable ArchUnit rules for modular monoliths | Modularity toolkit for Spring Boot applications |
| Architecture verification | Runs during tests | Runs during tests; optional startup verification is also available |
| Production classpath | Nothing added when used in test scope | Verification can be used in test scope; runtime features require corresponding production dependencies |
| Framework dependency | Framework-independent core; optional Spring rules | Designed for Spring Boot applications |
| Module configuration | Fluent Java API, YAML, or package conventions | Package conventions, annotations, and customization APIs |
| Built-in structural checks | Allowed dependencies, internal-package protection, API-only access, and cycles | Module cycles, internal access, and explicitly allowed dependencies |
| Additional architecture policies | Directional communication contracts and Spring-specific rules for controllers, repositories, transactions, events, and injection | Extensible verification with support for additional checks |
| Module integration testing | Not provided | Supported |
| Runtime event publication and observability | Not provided | Available through optional modules |
The two can be used together when Modulith Rules provides additional policies you need. If Spring Modulith already covers your requirements, adding another library may be unnecessary. See the Spring Modulith documentation for its verification model and available features.
Common questions
Short answers to what people ask before adopting the library.
What is a modular monolith?
A single deployable application whose code is divided into modules with explicit boundaries. Each module owns its data and exposes a narrow API; other modules go through that API rather than reaching into internal classes. You get the independence of service boundaries without distributed transactions, network failure modes or separate deployment pipelines.
How do you enforce module boundaries in Java?
With an architecture test that fails the build when a boundary is crossed. Modulith Rules provides that test: declare your modules, then check the rules against your compiled classes with ArchUnit. Package-private visibility and Java modules help, but neither expresses allowed dependencies between modules or detects cycles.
Should I use Modulith Rules or Spring Modulith?
Choose Modulith Rules when you want configurable ArchUnit policies with a framework-independent core, Java or YAML configuration, and its specific communication-contract or Spring architecture checks. Choose Spring Modulith when you want a broader Spring Boot modularity toolkit, including structural verification, module integration testing, documentation and optional runtime features. Both support architecture verification during tests and can be used together when additional policies are useful.
Does it work without Spring?
Yes. modulith-rules-core carries the boundary, cycle and communication rules and has no Spring dependency. The Spring rules live in a separate artifact you add only if you want them, and its Spring dependencies are provided scope.
Will it slow down my build?
The cost is ArchUnit's class import, which happens once per test class and is cached across rules in the same @AnalyzeClasses run. On a codebase of a few thousand classes the whole rule set adds a few seconds to the test phase.
How do I adopt it on an existing codebase?
Start with cycle detection alone, which needs no configuration beyond module names. Then declare allowedDependencies one module at a time: a module with none declared is left unrestricted, so you can tighten boundaries incrementally without a red build on day one.