Konture Architecture Tests
Konture (https://github.com/baole/konture) combines the Gradle build graph with AST/PSI static analysis to give whole-project/whole-graph visibility — the one thing compilers and linters structurally cannot provide, since they only ever see a single compile-unit or a single file’s AST. Architecture tests run as fast, ordinary Kotlin unit tests.
Prerequisites
Before writing any rule, confirm:
- The project is a Kotlin/Gradle project (Android, KMP, or JVM backend).
- Konture is (or can be) added as a dependency — check
gradle/libs.versions.tomlor rootbuild.gradle.ktsfor whether it’s already applied, and what version is resolved. - There’s a real question to answer: “could a compiler or detekt/ktlint already catch this?” If yes, this doesn’t belong in Konture — redirect to a lint rule instead. Konture earns its keep specifically where whole-graph visibility is required.
Workflow to write a new architecture rule
Follow these steps in order, adapting to the task:
- Step 1: Align the request with foundational pillars or define custom guardrails
- Step 2: Discover the project’s real architecture (never assume one)
- Step 3: Locate or scaffold the dedicated test module
- Step 4: Verify the DSL, then draft the test
- Step 5: Run it and confirm it actually fails red on a real violation
Step 1. Align the request with foundational pillars or define custom guardrails
Read references/six_pillars.md for the full taxonomy (the six core foundational pillars, why compiler/linter misses them, and how to formulate project-specific custom guardrails such as naming conventions, legacy quarantine, serialization annotations, or concurrency restrictions).
Determine whether the request maps to one of the six core architectural pillars, or if it represents a custom project-specific guardrail. Be open and flexible: architecture tests depend entirely on each project’s unique engineering standards. If a request doesn’t cleanly fit one of the core pillars, design it as a custom project-specific guardrail instead of forcing it.
Step 2. Discover the project’s real architecture
Every project’s layering, module names, and DI framework are different — never reuse a layering scheme from a previous project or from any example in this skill as if it were this project’s actual architecture. Do this in order, stopping as soon as you have enough to proceed confidently:
- Look for an explicit architecture doc first —
docs/architecture.md, anadr/ordocs/decisions/folder (Architecture Decision Records),CONTRIBUTING.md, a module-graph diagram, or a “modularization strategy” doc referenced from the rootREADME.md. If one exists, treat it as the source of truth for intended layering — it may state rules the codebase doesn’t fully enforce yet, which is exactly the gap Konture should close. - If no doc exists, infer from the real build graph — actual module names and their
implementation/apidependencies insettings.gradle.ktsand each module’sbuild.gradle.kts, actual package conventions in use, the DI framework actually present (Koin/Hilt/Spring/ Dagger), and what layering the existing module graph already implies. - If the intended layering still isn’t clear from either — ask the user directly rather than guessing. A wrong guess produces a rule that either enforces the wrong policy or has to be quietly loosened later, which defeats the point of an architecture guardrail. A short, targeted question (“should
feature:checkoutbe allowed to depend onfeature:profileat all, or is that the sideways dependency you want blocked?”) is better than a confident-sounding rule built on an assumed shape. - Prefer wildcard & pattern matching: Avoid hardcoding long lists of full individual module paths or package names. Use wildcard/pattern selectors (
haveNameMatching(":feature:**"),haveNameMatching(":core:domain**"),resideInAPackage("..domain..")) so rules automatically scale and cover newly added modules or packages.
Step 3. Locate or scaffold the dedicated test module
Konture’s stated best practice is a separate module (commonly :konture-test or :architecture-tests), not architecture tests living inside production modules. If it doesn’t exist yet:
- Create the module and register it in
settings.gradle.kts. - Use resources/konture-test-module.build.gradle.kts.template as the starting
build.gradle.kts. Ensure the Konture plugin (alias(libs.plugins.konture)orid("io.github.baole.konture")) is applied inplugins { ... }. Check which testing framework/libraries the target project uses (Kotest, JUnit 4/5,kotlin.test, etc.) and reuse those existing dependencies. Do not addproject(":...")dependencies for production modules, as Konture discovers multi-module layout automatically.
Step 4. Verify the DSL, then draft the test
Read references/dsl_verification.md before writing real code. Konture’s own docs use two different, likely-inconsistent DSL syntaxes — that file explains how to check which one actually compiles against the version resolved in this project.
Use resources/ArchitectureGuardrails.kt.template as the starting shape for the test class, then:
- Use wildcard/pattern matching (
haveNameMatching(":feature:**"),haveNameMatching(":core:domain**"),resideInAPackage("..domain..")) rather than hardcoding explicit full module/package lists. - Name each test after the policy, not the mechanism — e.g.
`repositories inside domain must be declared as interfaces`rather thantest1. The test name is the architecture’s live documentation. - Keep one rule per test (“one rule, one reason to fail”) — don’t bundle unrelated assertions into a single test, or a red CI run won’t tell you which policy actually broke.
Step 5. Run it and confirm it fails red on a real violation
A rule that’s never seen a real violation is unverified. Where possible, temporarily introduce the violation it’s meant to catch (or point to an existing one, if the codebase already has the smell) to confirm the rule actually fails red before it’s trusted to fail red in CI. Then remove the temporary violation (or leave it as a tracked, documented exception if the user chooses that instead of fixing it immediately).
Common judgment calls to flag to the user, not decide silently
See the “Common judgment calls” section at the end of references/six_pillars.md — covers overly broad wildcard bans, KMP commonMain platform-leak rule scoping, DI dead-binding false positives, and how to handle a rule that the codebase already violates.
Reference files
- references/six_pillars.md — foundational pillars, custom guardrails, quick lookup, and judgment calls to flag rather than decide silently.
- references/dsl_verification.md — the README-vs-docs DSL discrepancy and how to verify which syntax compiles.
- resources/konture-test-module.build.gradle.kts.template — starter Gradle module config.
- resources/ArchitectureGuardrails.kt.template — starter test class.