There's a moment that every developer on a growing Kotlin or Android project will recognize.
A new engineer joins the team. They want to understand how the system is structured before they write their first line of code. So they open the project. They see 30 modules. They ask: "Where should I start?"
And nobody has a good answer.
The architecture diagram on the wiki is from 18 months ago. The tech spec references modules that were renamed twice. The most honest answer is: "Just dig into the Gradle files and figure it out."
I've been on both sides of that conversation. And it bothered me enough that I decided to fix it.
The Problem Nobody Names Directly
When you're working on a 5-module project, architecture is obvious. Everything fits in your head.
Past 15–20 modules, something changes. The system becomes too large to hold in working memory. And because Gradle builds succeed even when your dependency graph is a mess, the degradation is silent. There's no compiler warning for "your data layer now depends on your presentation layer." No test failure for "two feature modules have secretly coupled to each other."
By the time someone notices - usually during a painful refactoring, a CI build time regression, or an attempt to extract a feature into a separate app - the technical debt has compounded for months.
The root cause is that the real architecture of a multi-module project lives inside the Gradle dependency graph, not in any diagram, doc, or spec.
What Already Exists (And Why It Wasn't Enough)
I looked at every tool in the Kotlin ecosystem before building anything.
Dependency Analysis Plugin by autonomousapps is excellent - but it focuses on unused dependencies and ABI surface. It tells you what you don't need. Not what your structure looks like.
Module Graph Assert by jraska generates Graphviz graphs and does have a CI enforcement layer - assertModuleGraph can fail the build using regex-based rules and max tree height checks. The gap is elsewhere: no interactive visualization, no historical tracking, and KMP support requires manually adding source set configuration because the plugin has no native awareness of KMP module boundaries.
ArchUnit is the closest thing to what I wanted. But it's Java-centric, has no visualization layer, and building meaningful tests against a Gradle project structure requires significant boilerplate that feels out of place in a Kotlin codebase.
None of them do all three things: visualize, enforce, and track. Not together, not in a single tool, not with KMP awareness. That gap is what Aalekh is designed to fill.
What Aalekh Does
Two lines to set up:
// settings.gradle.kts
plugins {
id("io.github.shivathapaa.aalekh") version "0.1.0"
}
One command to run:
./gradlew aalekhReport
A self-contained HTML file opens in your browser. No server. No CDN. No internet connection required. The entire report - including the D3.js visualization engine and all your graph data - is embedded in a single file you can share over Slack or attach to a PR.
Want to see it before installing anything?
- Now in Android - Google's reference Android app
- Same project with a cyclic dependency injected
- Tallyo - a Kotlin Multiplatform project
The interactive graph
Every module is a node. Every dependency is an edge. Node color tells you the module type at a glance - Android App is blue, KMP is purple, JVM Library is amber.
The useful part is what gets highlighted:
- Circular dependency nodes pulse with a red ring
- God modules (high fan-in AND fan-out) glow orange
- The critical build path is traced in purple
You can filter edges by type, click any node to open the module inspector, and see fan-in, fan-out, instability index, and transitive dependency count per module.
Cycle detection that doesn't lie to you
One decision I spent real time on: test dependencies in cycle detection.
Consider this common pattern:
:core:datastore → (nothing)
:core:datastore-test → :core:datastore (testImplementation)
Naive cycle detection flags this as a cycle. It isn't - it's a test fixtures module depending on the module it provides fixtures for.
Aalekh separates main-code cycles (real architectural errors, aalekhCheck fails on these) from test-only cycles (shown as info, never fail the build). This distinction means you can actually trust the violations you see.
Architecture metrics
The Metrics panel computes per-module numbers that turn subjective architectural conversations into objective data:
| Metric | What it tells you |
|---|---|
| Fan-out | How many modules this module directly depends on |
| Fan-in | How many modules depend on this one |
| Instability |
fanOut / (fanIn + fanOut) - 0 is stable, 1 is unstable |
| Transitive deps | Total modules reachable from this one |
| Critical path | Longest dependency chain - constrains build parallelism |
| God modules | High fan-in AND fan-out - hardest to change safely |
These aren't vanity metrics. They're signals for where refactoring attention should go first.
CI enforcement
./gradlew aalekhCheck
Fails the build on violations. Automatically wired into ./gradlew check. Writes:
-
aalekh-results.xml- JUnit XML, works with every CI system natively -
aalekh-results.json- full machine-readable report with graph, summary, violations, and metadata
GitHub Actions:
- name: Run architecture check
run: ./gradlew aalekhCheck
- name: Upload Aalekh report
uses: actions/upload-artifact@v4
if: always()
with:
name: aalekh-report
path: build/reports/aalekh/
How Aalekh Is Built Internally
I tried to apply the same principles to Aalekh's own structure that Aalekh helps you enforce in your project:
aalekh-model ← Data classes only. Zero dependencies.
aalekh-analysis ← Pure Kotlin. No Gradle API. Graph algorithms.
aalekh-report ← Report generators. No Gradle API.
aalekh-gradle ← Gradle API lives here and nowhere else.
The Gradle API only appears in the outermost module. Everything in aalekh-analysis and aalekh-report is pure Kotlin - fully unit-testable without Gradle on the classpath. The DFS cycle detection, topological sort, and instability computation all run in milliseconds in plain JUnit 5. Only the Gradle integration tests are slow.
Configuration cache compatibility
Gradle 9.x enables configuration cache by default. Task actions run in a separate JVM from project configuration - you cannot capture live Project, Configuration, or Dependency objects inside a task.
The solution: all dependency data is serialized to plain String maps during configuration (inside provider { } lambdas), passed as @Input primitives to AalekhExtractTask, which writes graph.json. Report and check tasks read from that file. No live Gradle objects survive into any task action.
Configuration phase:
provider { buildSubprojectData(rootProject) } ← runs at config time
→ Map<String, List<String>> ← plain strings, CC-safe
Execution phase:
AalekhExtractTask → writes graph.json
AalekhReportTask → reads graph.json → writes index.html
AalekhCheckTask → reads graph.json → writes results.xml + results.json
The intermediate graph.json is also what makes the tool extensible - anyone can build additional tooling on top of it without touching the plugin.
What's Coming in v0.2.0
The current release covers visualization and cycle enforcement. Next up: layer enforcement via a Kotlin DSL - the thing I originally built this for.
aalekh {
layers {
layer("domain") {
modules(":core:domain", ":feature:*:domain")
}
layer("data") {
modules(":core:data", ":feature:*:data")
canOnlyDependOn("domain")
}
layer("presentation") {
modules(":feature:*:ui", ":app")
canOnlyDependOn("domain", "data")
}
}
featureIsolation {
featurePattern = ":feature:**"
}
}
When :feature:checkout:data adds implementation(project(":feature:checkout:ui")), the build fails with a message pointing to the exact file and line to fix.
Teams with existing violations can migrate gradually using severity overrides:
rules {
rule("layer-dependency") {
severity = Severity.WARNING // see violations without blocking CI yet
}
}
Promote to ERROR when you're ready to enforce.
Requirements
- Gradle 9.0+
- JVM 11+ (v0.1.1 fixes a crash that affected JDK 11–20)
- Kotlin 2.3+
- AGP 9.x+ (if Android)
- Kotlin DSL only —
settings.gradle.ktsrequired (Groovy not supported)
Try It
// settings.gradle.kts
plugins {
id("io.github.shivathapaa.aalekh") version "0.1.0"
}
./gradlew aalekhReport
shivathapaa
/
Aalekh
Aalekh is a Gradle plugin that extracts, visualizes, and enforces architectural rules across any Gradle multi-module project - Kotlin Multiplatform, Android, JVM, or any Gradle project.
Aalekh
Architecture Visualization & Linting for Gradle Multi-Module Projects
Aalekh is a Gradle plugin that extracts, visualizes, and enforces architectural rules across any Gradle multi-module project - Kotlin Multiplatform, Android, JVM, or any Gradle projects. It gives teams three capabilities that no existing tool provides together: an interactive module graph, a Kotlin DSL for architecture rule enforcement, and historical metrics tracking.
Sample Reports
-
Now in Android App
-
Now in Android App - with cyclic dependency
-
Tallyo (KMP)
Sample Report Demo
Why Aalekh?
Tool
Visualizes
Enforces rules
Tracks metrics
KMP-aware
Aalekh
✓
✓
✓
✓
Aalekh visualizes, enforces, and tracks - in a single plugin, with zero external dependencies beyond the browser.
Quick Start
1. Add to settings.gradle.kts:
plugins {
id("io.github.shivathapaa.aalekh") version "<latest-version>"…Free, open source (Apache 2.0), actively maintained. If you run it on your project and something breaks - or works better than expected - I'd genuinely like to know in the comments.
The next post will cover how the layer rule engine works under the hood - glob pattern resolution, violation message generation, and the severity migration system. Follow to get notified.
Top comments (0)