Open-source project
jraska/modules-graph-assert avatar
jraska/modules-graph-assert

modules-graph-assert turns your Gradle module tree into a rule set

Gradle plugin to keep your modules graph healthy and lean.

645 stars36 forksKotlinApache-2.0

At a glance

What is it?
The jraska/modules-graph-assert plugin checks a Gradle project's module dependencies against regex rules, a maximum tree height and an optional alias based module typing, so a module graph that is quietly turning into a list fails the build instead of slowing it down.
Who is it for?
Adopt modules-graph-assert when your Gradle build has enough modules that dependency drift is a real cost and you want the graph policed in CI by a check task. Do not adopt it to diagnose a graph you have not first drawn, and do not mix module names with moduleNameAssertAlias in the same rule set, which the README advises against.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The plugin asserts on a graph you describe with regex rules

The problem this solves is not dependency resolution but dependency shape. The README argues that build speed can be very dependent on the structure of the module graph, that modules exist to separate logical units and enforce proper dependencies, and that the graph can silently degenerate into a list like structure. Breaking a problematic dependency once it exists is described as very difficult, so preventing it is cheaper. The enforcement mechanism is a set of rules you write yourself, checked by a task called assertModuleGraph that runs as part of the check task. The rules are regular expressions matched against module names, and the syntax distinguishes two directions. An allowed rule uses a regex -> regex signature, for instance ':feature-one -> :feature-[a-z-:]*', ':.* -> :core' and ':feature.* -> :lib.*'. A dependency that matches none of the allowed rules fails the assertion. The other form, restricted, uses a regex -X> regex signature, such as ':feature-[a-z]* -X> :forbidden-to-depend-on', for rules where one thing must never reach another.

maxHeight is the metric that catches the list in disguise

Beyond pairwise rules the plugin offers a structural check. Setting maxHeight = 4 verifies that the height of the modules tree, with a root in the module, does not exceed 4. The README makes the argument for that metric directly: tree height is a good metric to prevent the module tree from degenerating into a list. That is the mechanism most worth understanding, because it catches a class of problem that no pairwise rule would. Consider a project with forty modules arranged as a chain, where each one depends on the next. No single edge looks wrong, so allowed rules would all pass, yet the build pays for a deep linear chain. A height bound fails that shape regardless of what the individual dependencies are named. The height is measured from a root module, which is why the plugin is typically applied in the final app module rather than in a leaf library.

Applying the plugin and running the first assertion

The plugin is applied to the module whose dependency graph you want to assert, using the standard Gradle plugins block.

groovy
plugins {
  id "com.jraska.module.graph.assertion" version "2.9.1"
}

The README then gives two ways to run the checks.

bash
- You can run `./gradlew assertModuleGraph` to execute configured checks or `./gradlew check` where `assertModuleGraph` will be included.

Running check is the interesting one, because assertModuleGraph is included there, which means the assertion runs as part of the normal verification lifecycle and can gate a merge. There is also a configuration option, assertOnAnyBuild = true, which runs the checks on every Gradle build without needing an explicit task, and the README points to a pull request for the details of that change. The rules themselves live in a moduleGraphAssert block.

groovy
moduleGraphAssert {
  maxHeight = 4
  allowed = [':.* -> :core', ':feature.* -> :lib.*'] // regex to match module names
  restricted = [':feature-[a-z]* -X> :forbidden-to-depend-on'] // regex to match module names
  configurations = ['api', 'implementation'] // Dependency configurations to look. ['api', 'implementation'] is the default
  assertOnAnyBuild = false // true value will run the assertions as part of any build without need to run the assert* tasks, false is default
}

Rules are applied on the Gradle module and its api and implementation dependencies by default, and the configurations key lets you change which dependency configurations are inspected.

moduleNameAssertAlias lets rules speak in module types

The hardest part of writing dependency rules is that module names encode history rather than intent. The alias mechanism exists to sidestep that. Instead of relying on module names, you set a property ext.moduleNameAssertAlias on a module, and the allowed and restricted rules then use that alias instead of the module name. The README suggests a flat module graph built on three types, where each module sets ext.moduleNameAssertAlias to Api, Implementation or App, and the rules become 'Implementation -> Api', 'App -> Implementation' and 'App -> Api'. That is a different style of rule from the project name based syntax, and it survives renaming. There is also a migration path. A module can carry the alias NeedsMigration while rules still permit 'NeedsMigration -> .*' and '.* -> NeedsMigration', and those modules can then be moved one by one into Implementation or Api. One constraint is explicit: use either module names or moduleNameAssertAlias everywhere, mixing both is not recommended.

Graphviz export and statistics give you the picture before the verdict

Before you write rules, the plugin can show you the graph. A helper task named generateModulesGraphvizText produces a graph of dependent modules, and the longest path of the project is drawn in red. That red path is the visual counterpart of the maxHeight check, and it is the fastest way to see which chain of modules is making your build slow. The task accepts project parameters for narrowing the output.

bash
./gradlew generateModulesGraphvizText -Pmodules.graph.of.module=:feature-one
./gradlew generateModulesGraphvizText -Pmodules.graph.output.gv=all_modules

The first restricts the output to a subgraph rooted at one module, the second writes the GraphViz file to a chosen path. A second task, generateModulesGraphStatistics, prints information about the graph: Modules Count, Edges Count, Height and Longest Path. Both tasks come with the same caveat, that if you use Gradle configuration on demand you should pass --no-configure-on-demand, otherwise the graph is incomplete.

Configuration on demand can hide modules and produce false passes

The most likely way to be misled by this plugin is Gradle's own configuration on demand feature. The README warns that it may hide some modules from the plugin visibility, and that if you notice some modules are missing you should try the --no-configure-on-demand flag. That is the failure mode worth internalising. A partially visible graph can satisfy every rule you wrote and still contain the dependency you were trying to ban, because the module that introduced it was never configured and therefore never analysed. The same caveat is repeated for the graph visualisation tasks, which means an incomplete graph also produces a misleading picture, not just a misleading pass. The interaction between a plugin that reads the resolved project and a build feature that resolves lazily is the sharp edge here, and no rule you write can protect against it. If you adopt the plugin, run it once with --no-configure-on-demand and compare the statistics before trusting a green build.

The alternative is a strict dependency plugin, with a different cost

For Gradle projects the common alternative to a graph rule plugin is a strict dependency checking plugin, which rejects a declared dependency outright rather than asserting an allowed shape. The difference in approach matters for what you can express. A strict dependency plugin works per dependency declaration and gives you a hard failure when someone adds an edge you forbade, but it says nothing about the resulting structure, so it cannot express a height bound or tell you that your graph has collapsed into a chain. This plugin works on the resolved graph rather than the declaration list, which is why it can count edges, compute height and report a longest path, and why it also inherits the configuration on demand problem, since the resolved graph is exactly what that build feature can leave incomplete. The trade is expressiveness against resolution cost. The plugin also brings nothing to a single module project, where there is no graph to protect and no rule that could fail.

Editorial conclusion

Adopt modules-graph-assert when your Gradle build has enough modules that dependency drift is a real cost and you want the graph policed in CI by a check task. Do not adopt it to diagnose a graph you have not first drawn, and do not mix module names with moduleNameAssertAlias in the same rule set, which the README advises against. Verify first by running ./gradlew generateModulesGraphvizText to see the shape of your graph, then add the plugin at version 2.9.1 and run ./gradlew assertModuleGraph, and if modules appear missing, retry with --no-configure-on-demand, since Gradle configuration on demand can hide modules from the plugin.

Frequently asked questions

How do I apply the modules-graph-assert plugin in a Gradle build?

Apply it to the module whose dependency graph you want to assert, using id "com.jraska.module.graph.assertion" version "2.9.1" in the plugins block. The README notes that you would typically apply it in your final app module, though configuration for any module is possible.

How do I run the module graph assertions in CI?

Run ./gradlew assertModuleGraph to execute the configured checks, or ./gradlew check, which includes assertModuleGraph. Setting assertOnAnyBuild = true makes the assertions run as part of every Gradle build without an explicit task.

What is the difference between allowed and restricted rules in modules-graph-assert?

Allowed rules use a regex -> regex signature and define the only permitted dependencies between modules, so any dependency matching none of those rules fails the assertion. Restricted rules use a regex -X> regex signature for defining custom rules such as ':feature-[a-z]* -X> :forbidden-to-depend-on'.

Why are some modules missing from my module graph?

The README states that Gradle configuration on demand may hide some modules from the plugin visibility. If modules appear missing, use the --no-configure-on-demand flag, which is also recommended for the generateModulesGraphvizText task.

What does the generateModulesGraphStatistics task print?

Executing generateModulesGraphStatistics prints information about the graph, specifically Modules Count, Edges Count, Height and Longest Path.

What licence is modules-graph-assert released under?

The project is licensed under Apache-2.0, with the LICENSE file at the top level of the repository. The plugin is published on the Gradle Plugin Portal as com.jraska.module.graph.assertion.

Official sources

  1. jraska/modules-graph-assert on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jraska-modules-graph-assert.svg)](https://hysenlabs.com/projects/jraska-modules-graph-assert)

Was this useful?