io.modularmonolith · test-scope library

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.

Read the getting started guide Source on GitHub
Current release
Versionv0.2.0 Group idio.modularmonolith Java17 or newer ArchUnit1.4.2 JUnit6.1.2 Spring Boot4.1.0 (optional) LicenseApache 2.0
01 · Scope
Test-time only

Rules run during mvn test. Nothing ships to production, and the core module has no Spring dependency.

02 · Coverage
18 rules, four families

Boundaries, cycles and communication contracts in the core module; controllers, repositories, transactions, events and injection in the Spring module.

03 · Configuration
Fluent, YAML or convention

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();
}

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

7 rules · core
modulesOnlyDependOnAllowedModules()
Only declared dependencies are used
internalsShouldNotBeAccessedFromOutside()
Internal packages stay internal
crossModuleAccessOnlyThroughApi()
Cross-module calls go through API packages
moduleShouldHaveNoDependencies(name)
A module has zero outgoing dependencies
moduleShouldOnlyBeAccessedBy(name, ...)
Only the listed modules may reach it
onlyAllowedDependenciesAreUsed()
Alias for the allowed-dependency rule
noModuleAccessesInternalsOfOthers()
Alias for the internal-protection rule

Cycle rules

3 rules · core
noModuleCycles()
No circular dependencies between modules
moduleHasNoCycles(name)
One module takes part in no cycle
noCircularDependenciesBetweenModules()
Alias for noModuleCycles()
Not a rule, but useful: CycleRules.detectCycles(graph) is public and static, so you can run the DFS against a hand-built graph without importing any classes.

Communication rules

3 rules · directional contracts
asyncModulesShouldNotCallDirectly()
An ASYNCHRONOUS contract forbids direct calls
noCommModulesShouldNotInteract()
A NONE contract forbids any dependency
allCommunicationContractsRespected()
Both of the above, in one rule

Spring rules

5 rules · modulith-rules-spring
controllersShouldNotCrossModuleBoundaries()
Controllers do not depend on other modules' controllers
repositoriesShouldBeModuleInternal()
Repositories are reached from their own module only
transactionalMethodsShouldNotSpanModules()
Transactions do not cross a boundary
eventClassesShouldBeInApiPackages()
Shared events live in API packages
noDirectInjectionOfInternalBeans()
No cross-module injection past the public API

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. 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

Spring Modulith is a runtime framework covering module verification, observability and event publication. Modulith Rules runs only at test time. You can use both: Spring Modulith for runtime isolation, Modulith Rules for build-time enforcement.

Concern Modulith Rules Spring Modulith
When it runsTest phase, during mvn testApplication runtime
Production classpathNothing addedRuntime dependency
Framework couplingCore module has noneSpring Boot
Rule authoringRules ship ready to useVerification model, custom checks written by hand
Non-Spring projectsSupported through the core moduleOut of scope
Getting started Configuration reference GitHub

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?

Use Spring Modulith when you want runtime module isolation, an event publication registry and observability in a Spring Boot application. Use Modulith Rules when you want build-time enforcement with nothing added to the production classpath, or when the project is not a Spring project. They run at different times and can be used together.

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.